Handler造成内存泄露算是一个比较常见的问题,今天我们从字节码层面来探究哈,为啥handler会造成内存泄露?
要将java代码转为smali(android虚拟机字节码的解释语言),需要安装as插件
java2smali: File—>Settings—>Plugins—>Marketplace—>搜索“java2smali”—>安装
使用:打开要转的类文件—>Build菜单—>Compile to Smali
内部类如何持有外部应用?
public class Test2Activity extends Activity {
Handler mHandler = new Handler(){
@Override
public void handleMessage(@NonNull Message msg) {
super.handleMessage(msg);
finish();
}
};
}
上面这段代码,就是平时我们使用Handler时的声明,那我们通过反编译来看哈,反编译后生成了两个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”对象, synthetic 代表“this$0”对象不是原生的,而是生成的
# instance fields
.field final synthetic this$0:Lcom/test/Test2Activity;
# direct methods
.method constructor <init>(Lcom/test/Test2Activity;)V
#声明初始化方法需要2个寄存器(下面的p0和p1)
.registers 2
#参数1
.param p1, "this$0" # Lcom/test/Test2Activity;
.prologue
.line 16
#通过iput-object指令,将p1(构造器传入的第一个参数)赋值给this$0
iput-object p1, p0, Lcom/test/Test2Activity$1;->this$0:Lcom/test/Test2Activity;
#通过invoke-direc指令,调用原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;
#调用v0的finish方法
invoke-virtual {v0}, Lcom/test/Test2Activity;->finish()V
.line 21
return-void
.end method
这里为了减少篇幅去掉了空行,class为“Test2Activity$1”,“super ”父类是Handler, source来源是“Test2Activity.java”这个类,运行时mHandler对象实际是“Test2Activity$1”,“Test2Activity$1”继承了Handler,这就是它叫匿名内部类原因。在看看它init方法,初始化传入“Lcom/test/Test2Activity;”,翻译成java代码构造方法就是“Test2Activity$1(Test2Activity mActivity)”
梳理哈初始化方法,和调用finish的方法
- 通过“.field”指令声明类型为“Lcom/test/Test2Activity”的全局变量“this$0“
构造方法:
- 通过“.registers”指令声明需要的寄存器地址2个,p1(第一个参数), p0(相当于当前对象this,如果是静态方法p0就是第一个参数)
- 通过“.param”接收第一个参数存入p1
- 通过“iput-object”指令将p1赋值给声明的全局变量this&0,相当于java中的this.a = a
- 通过“invoke-direct”指令调用原Handler的init方法,相当于java的super()方法
到这里就初始化完了,下面看看如何调用finish方法的
- 通过“ iget-object”指令,将this/&0存入v0寄存器
- 通过“invoke-virtual”指令,调用v0的finish方法
关于调用方法指令,invoke-virtual(调用普通方法),invoke-direct(私有方法,和初始化方法),在smali中方法调用有很多指令,静态和非静态都不同,有兴趣可以自己单独去了解哈,再看哈在Test2Activity这个类中如何去做初始化的呢
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
#声明寄存器个数
.registers 2
#函数的起点
.prologue
#行数
.line 15
#当前activity调用初始化
invoke-direct {p0}, Landroid/app/Activity;-><init>()V
.line 16
#创建Handler的匿名对象,并存入v0中
new-instance v0, Lcom/test/Test2Activity$1;
#调用v0的init方法,将p0传入,p0代表(this当前actvity对象)
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
总结:
- 匿名内部类执行时,会声明一个类型为外包类的this/&0对象
- 匿名内部类初始化方法,多了一个参数,当前外部类
- 外部对象在初始化内部类时,会传入自身对象
即使内部类持有外部应用,也不能说明会造成内部泄露啊,android的GC回收算法都可以分分钟解决!
确实,只是声明不会找内存泄露。下面将演示如何造成内存泄露
Handler内存泄露的原因
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();
}
}
上面这种写法就会造成内存泄露,先看哈内泄漏后堆栈信息
从图中的应用关系可知MessageQueue的应用导致Test2Activity无法回收,为啥MessageQueue会有Test2Activity的应用?带着这个疑问去查哈源码
Handler.java
public Handler(@Nullable Callback callback, boolean async) {
..省略数行代码
mLooper = Looper.myLooper();
mQueue = mLooper.mQueue;
..省略数行代码
}
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);
}
整个调用链:
- Message对象池用obtain方法创建了一个message对象
- 给message.target复制this,this对象就是当前匿名内部类Handler
- 调用MessageQueue的enqueueMessage,将Message放入队列
queue的最终引用对象是Looper,我们在看看Looper
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);
}
到这里就可以确定为啥会造成内存泄露了,因为我们发送了一个延迟消息到Looper的MessageQueue,Looper的持有对象是sThreadLocal , 在引用启动时,通过main方法的调用了Looper.prepare进行实例化,主线程对应的Key和Looper就存入了sThreadLocal 中。我们关闭activity时消息未被处理,消息对象的target持有当前activity。GC没办法回收sThreadLocal ,因为他持有主线程引用,也没有办法回收Looper,所以MessageQueue、Message和Message持有的匿名Handler,匿名Handler持有的Activity都没办法回收。
解决方法
- 静态的声明Handler,这种方法虽然可以解决不持有Activity的问题,但是不能调用非静态方法。
- onDestroy的时候清理掉方法handler.removeCallbacksAndMessages(null);
总结
到这里我们就已经了解了匿名内部类导致内存泄露的问题了,本身并不会导致内存泄露,只是持有类的对象不可回收导致了内存泄露。