Android 匿名內(nèi)部類造成的內(nèi)存泄露分析

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

  1. 通過“.field”指令聲明類型為“Lcom/test/Test2Activity”的全局變量“this$0“

構(gòu)造方法:

  1. 通過“.registers”指令聲明需要的寄存器地址2個(gè),p1(第一個(gè)參數(shù))嘹裂, p0(相當(dāng)于當(dāng)前對(duì)象this妄壶,如果是靜態(tài)方法p0就是第一個(gè)參數(shù))
  2. 通過“.param”接收第一個(gè)參數(shù)存入p1
  3. 通過“iput-object”指令將p1賦值給聲明的全局變量this&0,相當(dāng)于java中的this.a = a
  4. 通過“invoke-direct”指令調(diào)用原Handler的init方法,相當(dāng)于java的super()方法

到這里就初始化完了寄狼,下面看看如何調(diào)用finish方法的

  1. 通過“ iget-object”指令丁寄,將this/&0存入v0寄存器
  2. 通過“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)泄漏后堆棧信息


dump.png

從圖中的應(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)用鏈:

  1. Message對(duì)象池用obtain方法創(chuàng)建了一個(gè)message對(duì)象
  2. 給message.target復(fù)制this阵面,this對(duì)象就是當(dāng)前匿名內(nèi)部類Handler
  3. 調(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都沒辦法回收肺缕。

解決方法

  1. 靜態(tài)的聲明Handler,這種方法雖然可以解決不持有Activity的問題授帕,但是不能調(diào)用非靜態(tài)方法同木。
  2. onDestroy的時(shí)候清理掉方法handler.removeCallbacksAndMessages(null);

總結(jié)

到這里我們就已經(jīng)了解了匿名內(nèi)部類導(dǎo)致內(nèi)存泄露的問題了,本身并不會(huì)導(dǎo)致內(nèi)存泄露跛十,只是持有類的對(duì)象不可回收導(dǎo)致了內(nèi)存泄露彤路。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市芥映,隨后出現(xiàn)的幾起案子洲尊,更是在濱河造成了極大的恐慌,老刑警劉巖奈偏,帶你破解...
    沈念sama閱讀 217,406評(píng)論 6 503
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件坞嘀,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡惊来,警方通過查閱死者的電腦和手機(jī)丽涩,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,732評(píng)論 3 393
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)裁蚁,“玉大人矢渊,你說我怎么就攤上這事⊥髦ぃ” “怎么了矮男?”我有些...
    開封第一講書人閱讀 163,711評(píng)論 0 353
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)室谚。 經(jīng)常有香客問我毡鉴,道長(zhǎng),這世上最難降的妖魔是什么秒赤? 我笑而不...
    開封第一講書人閱讀 58,380評(píng)論 1 293
  • 正文 為了忘掉前任眨补,我火速辦了婚禮,結(jié)果婚禮上倒脓,老公的妹妹穿的比我還像新娘撑螺。我一直安慰自己,他們只是感情好崎弃,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,432評(píng)論 6 392
  • 文/花漫 我一把揭開白布甘晤。 她就那樣靜靜地躺著含潘,像睡著了一般。 火紅的嫁衣襯著肌膚如雪线婚。 梳的紋絲不亂的頭發(fā)上遏弱,一...
    開封第一講書人閱讀 51,301評(píng)論 1 301
  • 那天,我揣著相機(jī)與錄音塞弊,去河邊找鬼漱逸。 笑死,一個(gè)胖子當(dāng)著我的面吹牛游沿,可吹牛的內(nèi)容都是我干的饰抒。 我是一名探鬼主播,決...
    沈念sama閱讀 40,145評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼诀黍,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼袋坑!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起眯勾,我...
    開封第一講書人閱讀 39,008評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤枣宫,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后吃环,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體也颤,經(jīng)...
    沈念sama閱讀 45,443評(píng)論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,649評(píng)論 3 334
  • 正文 我和宋清朗相戀三年郁轻,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了歇拆。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 39,795評(píng)論 1 347
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡范咨,死狀恐怖故觅,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情渠啊,我是刑警寧澤输吏,帶...
    沈念sama閱讀 35,501評(píng)論 5 345
  • 正文 年R本政府宣布,位于F島的核電站替蛉,受9級(jí)特大地震影響贯溅,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜躲查,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,119評(píng)論 3 328
  • 文/蒙蒙 一它浅、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧镣煮,春花似錦姐霍、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,731評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)胯府。三九已至,卻和暖如春恨胚,著一層夾襖步出監(jiān)牢的瞬間骂因,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,865評(píng)論 1 269
  • 我被黑心中介騙來(lái)泰國(guó)打工赃泡, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留寒波,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 47,899評(píng)論 2 370
  • 正文 我出身青樓升熊,卻偏偏與公主長(zhǎng)得像俄烁,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子僚碎,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,724評(píng)論 2 354