Android 匿名内部类造成的内存泄露分析

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的方法

  1. 通过“.field”指令声明类型为“Lcom/test/Test2Activity”的全局变量“this$0“

构造方法:

  1. 通过“.registers”指令声明需要的寄存器地址2个,p1(第一个参数), p0(相当于当前对象this,如果是静态方法p0就是第一个参数)
  2. 通过“.param”接收第一个参数存入p1
  3. 通过“iput-object”指令将p1赋值给声明的全局变量this&0,相当于java中的this.a = a
  4. 通过“invoke-direct”指令调用原Handler的init方法,相当于java的super()方法

到这里就初始化完了,下面看看如何调用finish方法的

  1. 通过“ iget-object”指令,将this/&0存入v0寄存器
  2. 通过“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();
    }
}

上面这种写法就会造成内存泄露,先看哈内泄漏后堆栈信息


dump.png

从图中的应用关系可知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);
    }

整个调用链:

  1. Message对象池用obtain方法创建了一个message对象
  2. 给message.target复制this,this对象就是当前匿名内部类Handler
  3. 调用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都没办法回收。

解决方法

  1. 静态的声明Handler,这种方法虽然可以解决不持有Activity的问题,但是不能调用非静态方法。
  2. onDestroy的时候清理掉方法handler.removeCallbacksAndMessages(null);

总结

到这里我们就已经了解了匿名内部类导致内存泄露的问题了,本身并不会导致内存泄露,只是持有类的对象不可回收导致了内存泄露。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 202,802评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,109评论 2 379
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 149,683评论 0 335
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,458评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,452评论 5 364
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,505评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,901评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,550评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,763评论 1 296
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,556评论 2 319
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,629评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,330评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,898评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,897评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,140评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,807评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,339评论 2 342