Handler造成內(nèi)存泄露算是一個(gè)比較常見的問題噪生,今天我們從字節(jié)碼層面來(lái)探究哈,為啥handler會(huì)造成內(nèi)存泄露月培?
要將java代碼轉(zhuǎn)為smali(android虛擬機(jī)字節(jié)碼的解釋語(yǔ)言),需要安裝as插件
java2smali: File—>Settings—>Plugins—>Marketplace—>搜索“java2smali”—>安裝
使用:打開要轉(zhuǎn)的類文件—>Build菜單—>Compile to Smali
內(nèi)部類如何持有外部應(yīng)用?
public class Test2Activity extends Activity {
Handler mHandler = new Handler(){
@Override
public void handleMessage(@NonNull Message msg) {
super.handleMessage(msg);
finish();
}
};
}
上面這段代碼瞭稼,就是平時(shí)我們使用Handler時(shí)的聲明,那我們通過反編譯來(lái)看哈腻惠,反編譯后生成了兩個(gè)Class
Test2Activity$1.smali
.class Lcom/test/Test2Activity$1;
.super Landroid/os/Handler;
.source "Test2Activity.java"
# annotations
.annotation system Ldalvik/annotation/EnclosingClass;
value = Lcom/test/Test2Activity;
.end annotation
.annotation system Ldalvik/annotation/InnerClass;
accessFlags = 0x0
name = null
.end annotation
#“.field”指令聲明“ this$0”對(duì)象弛姜, synthetic 代表“this$0”對(duì)象不是原生的,而是生成的
# instance fields
.field final synthetic this$0:Lcom/test/Test2Activity;
# direct methods
.method constructor <init>(Lcom/test/Test2Activity;)V
#聲明初始化方法需要2個(gè)寄存器(下面的p0和p1)
.registers 2
#參數(shù)1
.param p1, "this$0" # Lcom/test/Test2Activity;
.prologue
.line 16
#通過iput-object指令妖枚,將p1(構(gòu)造器傳入的第一個(gè)參數(shù))賦值給this$0
iput-object p1, p0, Lcom/test/Test2Activity$1;->this$0:Lcom/test/Test2Activity;
#通過invoke-direc指令廷臼,調(diào)用原Handler的初始化方法
invoke-direct {p0}, Landroid/os/Handler;-><init>()V
return-void
.end method
# virtual methods
.method public handleMessage(Landroid/os/Message;)V
.registers 3
.param p1, "msg" # Landroid/os/Message;
.annotation build Landroidx/annotation/NonNull;
.end annotation
.end param
.prologue
.line 19
invoke-super {p0, p1}, Landroid/os/Handler;->handleMessage(Landroid/os/Message;)V
.line 20
#將上面的this$0變量賦值給v0
iget-object v0, p0, Lcom/test/Test2Activity$1;->this$0:Lcom/test/Test2Activity;
#調(diào)用v0的finish方法
invoke-virtual {v0}, Lcom/test/Test2Activity;->finish()V
.line 21
return-void
.end method
這里為了減少篇幅去掉了空行,class為“Test2Activity$1”绝页,“super ”父類是Handler荠商, source來(lái)源是“Test2Activity.java”這個(gè)類,運(yùn)行時(shí)mHandler對(duì)象實(shí)際是“Test2Activity$1”续誉,“Test2Activity$1”繼承了Handler莱没,這就是它叫匿名內(nèi)部類原因。在看看它init方法酷鸦,初始化傳入“Lcom/test/Test2Activity;”饰躲,翻譯成java代碼構(gòu)造方法就是“Test2Activity$1(Test2Activity mActivity)”
梳理哈初始化方法牙咏,和調(diào)用finish的方法
- 通過“.field”指令聲明類型為“Lcom/test/Test2Activity”的全局變量“this$0“
構(gòu)造方法:
- 通過“.registers”指令聲明需要的寄存器地址2個(gè),p1(第一個(gè)參數(shù))嘹裂, p0(相當(dāng)于當(dāng)前對(duì)象this妄壶,如果是靜態(tài)方法p0就是第一個(gè)參數(shù))
- 通過“.param”接收第一個(gè)參數(shù)存入p1
- 通過“iput-object”指令將p1賦值給聲明的全局變量this&0,相當(dāng)于java中的this.a = a
- 通過“invoke-direct”指令調(diào)用原Handler的init方法,相當(dāng)于java的super()方法
到這里就初始化完了寄狼,下面看看如何調(diào)用finish方法的
- 通過“ iget-object”指令丁寄,將this/&0存入v0寄存器
- 通過“invoke-virtual”指令,調(diào)用v0的finish方法
關(guān)于調(diào)用方法指令泊愧,invoke-virtual(調(diào)用普通方法)伊磺,invoke-direct(私有方法,和初始化方法)删咱,在smali中方法調(diào)用有很多指令屑埋,靜態(tài)和非靜態(tài)都不同,有興趣可以自己?jiǎn)为?dú)去了解哈痰滋,再看哈在Test2Activity這個(gè)類中如何去做初始化的呢
Test2Activity.smali
.class public Lcom/test/Test2Activity;
.super Landroid/app/Activity;
.source "Test2Activity.java"
# instance fields
.field mHandler:Landroid/os/Handler;
# direct methods
.method public constructor <init>()V
#聲明寄存器個(gè)數(shù)
.registers 2
#函數(shù)的起點(diǎn)
.prologue
#行數(shù)
.line 15
#當(dāng)前activity調(diào)用初始化
invoke-direct {p0}, Landroid/app/Activity;-><init>()V
.line 16
#創(chuàng)建Handler的匿名對(duì)象雀彼,并存入v0中
new-instance v0, Lcom/test/Test2Activity$1;
#調(diào)用v0的init方法硕蛹,將p0傳入幅垮,p0代表(this當(dāng)前actvity對(duì)象)
invoke-direct {v0, p0}, Lcom/test/Test2Activity$1;-><init>(Lcom/test/Test2Activity;)V
#將v0賦值給上面聲明的mHandler
iput-object v0, p0, Lcom/test/Test2Activity;->mHandler:Landroid/os/Handler;
return-void
.end method
# virtual methods
.method protected onCreate(Landroid/os/Bundle;)V
.registers 2
.param p1, "savedInstanceState" # Landroid/os/Bundle;
.annotation build Landroidx/annotation/Nullable;
.end annotation
.end param
.prologue
.line 25
invoke-super {p0, p1}, Landroid/app/Activity;->onCreate(Landroid/os/Bundle;)V
.line 26
return-void
.end method
總結(jié):
- 匿名內(nèi)部類執(zhí)行時(shí)链烈,會(huì)聲明一個(gè)類型為外包類的this/&0對(duì)象
- 匿名內(nèi)部類初始化方法努隙,多了一個(gè)參數(shù)贬循,當(dāng)前外部類
- 外部對(duì)象在初始化內(nèi)部類時(shí)肝谭,會(huì)傳入自身對(duì)象
即使內(nèi)部類持有外部應(yīng)用谴返,也不能說明會(huì)造成內(nèi)部泄露啊结榄,android的GC回收算法都可以分分鐘解決墩蔓!
確實(shí)梢莽,只是聲明不會(huì)找內(nèi)存泄露。下面將演示如何造成內(nèi)存泄露
Handler內(nèi)存泄露的原因
public class Test2Activity extends Activity {
Handler mHandler = new Handler(){
@Override
public void handleMessage(@NonNull Message msg) {
super.handleMessage(msg);
}
};
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
mHandler.sendEmptyMessageDelayed(0, 30 * 1000);
finish();
}
}
上面這種寫法就會(huì)造成內(nèi)存泄露奸披,先看哈內(nèi)泄漏后堆棧信息
從圖中的應(yīng)用關(guān)系可知MessageQueue的應(yīng)用導(dǎo)致Test2Activity無(wú)法回收昏名,為啥MessageQueue會(huì)有Test2Activity的應(yīng)用?帶著這個(gè)疑問去查哈源碼
Handler.java
public Handler(@Nullable Callback callback, boolean async) {
..省略數(shù)行代碼
mLooper = Looper.myLooper();
mQueue = mLooper.mQueue;
..省略數(shù)行代碼
}
public final boolean sendEmptyMessageDelayed(int what, long delayMillis) {
Message msg = Message.obtain();
msg.what = what;
return sendMessageDelayed(msg, delayMillis);
}
public final boolean sendMessageDelayed(@NonNull Message msg, long delayMillis) {
if (delayMillis < 0) {
delayMillis = 0;
}
return sendMessageAtTime(msg, SystemClock.uptimeMillis() + delayMillis);
}
public boolean sendMessageAtTime(@NonNull Message msg, long uptimeMillis) {
MessageQueue queue = mQueue;
if (queue == null) {
RuntimeException e = new RuntimeException(
this + " sendMessageAtTime() called with no mQueue");
Log.w("Looper", e.getMessage(), e);
return false;
}
return enqueueMessage(queue, msg, uptimeMillis);
}
private boolean enqueueMessage(@NonNull MessageQueue queue, @NonNull Message msg,
long uptimeMillis) {
msg.target = this;
msg.workSourceUid = ThreadLocalWorkSource.getUid();
if (mAsynchronous) {
msg.setAsynchronous(true);
}
return queue.enqueueMessage(msg, uptimeMillis);
}
整個(gè)調(diào)用鏈:
- Message對(duì)象池用obtain方法創(chuàng)建了一個(gè)message對(duì)象
- 給message.target復(fù)制this阵面,this對(duì)象就是當(dāng)前匿名內(nèi)部類Handler
- 調(diào)用MessageQueue的enqueueMessage轻局,將Message放入隊(duì)列
queue的最終引用對(duì)象是Looper,我們?cè)诳纯碙ooper
Looper.java
static final ThreadLocal<Looper> sThreadLocal = new ThreadLocal<Looper>();
private static void prepare(boolean quitAllowed) {
if (sThreadLocal.get() != null) {
throw new RuntimeException("Only one Looper may be created per thread");
}
sThreadLocal.set(new Looper(quitAllowed));
}
public static @Nullable Looper myLooper() {
return sThreadLocal.get();
}
ThreadLocal.java
public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
}
到這里就可以確定為啥會(huì)造成內(nèi)存泄露了样刷,因?yàn)槲覀儼l(fā)送了一個(gè)延遲消息到Looper的MessageQueue仑扑,Looper的持有對(duì)象是sThreadLocal , 在引用啟動(dòng)時(shí)置鼻,通過main方法的調(diào)用了Looper.prepare進(jìn)行實(shí)例化镇饮,主線程對(duì)應(yīng)的Key和Looper就存入了sThreadLocal 中。我們關(guān)閉activity時(shí)消息未被處理箕母,消息對(duì)象的target持有當(dāng)前activity储藐。GC沒辦法回收sThreadLocal 俱济,因?yàn)樗钟兄骶€程引用,也沒有辦法回收Looper钙勃,所以MessageQueue蛛碌、Message和Message持有的匿名Handler,匿名Handler持有的Activity都沒辦法回收肺缕。
解決方法
- 靜態(tài)的聲明Handler,這種方法雖然可以解決不持有Activity的問題授帕,但是不能調(diào)用非靜態(tài)方法同木。
- onDestroy的時(shí)候清理掉方法handler.removeCallbacksAndMessages(null);
總結(jié)
到這里我們就已經(jīng)了解了匿名內(nèi)部類導(dǎo)致內(nèi)存泄露的問題了,本身并不會(huì)導(dǎo)致內(nèi)存泄露跛十,只是持有類的對(duì)象不可回收導(dǎo)致了內(nèi)存泄露彤路。