美文网首页
Android 匿名内部类造成的内存泄露分析

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

作者: Ray206 | 来源:发表于2021-11-03 18:10 被阅读0次

    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);

    总结

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

    相关文章

      网友评论

          本文标题:Android 匿名内部类造成的内存泄露分析

          本文链接:https://www.haomeiwen.com/subject/vwwqzltx.html