[smali] This Handler class should be static or leaks might occur

掘金博客鏈接

相關(guān)demo源碼;

本文基于:
macOS:10.13/AS:3.4/Android build-tools:28.0.0/jdk: 1.8/apktool: 2.3.3

1. Handler內(nèi)存泄露測試

IDE提示

Activity 中創(chuàng)建 Handler 內(nèi)部類時,AS會給提內(nèi)存泄露提示及解決方案:

This Handler class should be static or leaks might occur (anonymous android.os.Handler)
Inspection info:Since this Handler is declared as an inner class, it may prevent the outer class from being garbage collected.
If the Handler is using a Looper or MessageQueue for a thread other than the main thread, then there is no issue.
If the Handler is using the Looper or MessageQueue of the main thread, you need to fix your Handler declaration, as follows:
   1. Declare the Handler as a static class;
   2. In the outer class, instantiate a WeakReference to the outer class and pass this object to your Handler when you instantiate the Handler;
   3. Make all references to members of the outer class using the WeakReference object.

先簡單測試下,運行如下代碼,然后手機多次進行橫豎屏切換,通過 AS 提供的 Profiler 監(jiān)控內(nèi)存變化:

// HandlerTestActivity.java
public class HandlerTestActivity extends AppCompatActivity {

    // 創(chuàng)建匿名Handler內(nèi)部類的對象
    private Handler leakHandler = new Handler() {
        @Override
        public void handleMessage(Message msg) {
            super.handleMessage(msg);
        }
    };

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_handler_test);

        leakHandler.postDelayed(new Runnable() {
            @Override
            public void run() {
                Logger.d("leakHandler 延遲執(zhí)行,內(nèi)存泄露測試");
            }
        }, 5 * 60 * 1000);
    }
}
profiler內(nèi)存監(jiān)控

內(nèi)存出現(xiàn)了明顯了升高;

簡單描述下原因:

  1. 由于上面的 Handler 內(nèi)部類定義在ui線程中,因此使用的主線程的 LooperMessageQueue;
  2. MessageQueue 中的 Message 會持有 Handler 對象;
  3. 匿名Handler內(nèi)部類對象持有著外部 Activity 的強引用;

以上三點導致當有 Message 未被處理之前, 外部類 Activity 會一直被強引用,導致即使發(fā)生了銷毀,也無法被GC回收;

因此處理方法通常有兩種:

  1. 在外部類 Activity 銷毀時取消所有的 Message,即 leakHandler.removeCallbacksAndMessages(null);
  2. 讓內(nèi)部類不要持有外部Activity的強引用;

AS給出的提示方案屬于第二種, 我們通過smali源碼來一步步探究驗證下;

2. 非靜態(tài)內(nèi)部類持有外部類的強引用

上面的 Java 代碼對應的 smali 源碼如下:

# HandlerTestActivity.smali
.class public Lorg/lynxz/smalidemo/ui/activity/HandlerTestActivity;
.super Landroid/support/v7/app/AppCompatActivity;
.source "HandlerTestActivity.java"

# 聲明了成員變量 `leakHandler`
# instance fields
.field private leakHandler:Landroid/os/Handler;

# direct methods
.method public constructor <init>()V
    .locals 1

    .line 20
    invoke-direct {p0}, Landroid/support/v7/app/AppCompatActivity;-><init>()V

    # `HandlerTestActivity$1` 是匿名內(nèi)部類, 此處創(chuàng)建了該類的一個對象,并將其賦值給 v0 寄存器
    .line 26
    new-instance v0, Lorg/lynxz/smalidemo/ui/activity/HandlerTestActivity$1;

    # p0 表示 `HandlerTestActivity` 對象自身
    # 此處表示調(diào)用 `HandlerTestActivity$1` 對象的 `init(HandlerTestActivity activity)` 方法
    invoke-direct {v0, p0}, Lorg/lynxz/smalidemo/ui/activity/HandlerTestActivity$1;-><init>(Lorg/lynxz/smalidemo/ui/activity/HandlerTestActivity;)V

    # 將 v0 寄存器的值賦值給了成員變量 `leanHandler`
    # 由于 `leanHandler` 變量的類型是 `Landroid/os/Handler;` , 可知 `HandlerTestActivity$1` 是 `Handler` 的子類
    # 結(jié)合上一句代碼,我們就知道 `HandlerTestActivity$1` 會以某種形式持有 `HandlerTestActivity` 的引用
    iput-object v0, p0, Lorg/lynxz/smalidemo/ui/activity/HandlerTestActivity;->leakHandler:Landroid/os/Handler;

    return-void
.end method

再來看看 HandlerTestActivity$1 類的代碼:

# HandlerTestActivity$1.smali
# 指明了本類 `HandlerTestActivity$1` 是 `Handler` 的子類
.class Lorg/lynxz/smalidemo/ui/activity/HandlerTestActivity$1;
.super Landroid/os/Handler;
.source "HandlerTestActivity.java"

# `EnclosingClass` 表明本類位于 `HandlerTestActivity` 中
# annotations
.annotation system Ldalvik/annotation/EnclosingClass;
    value = Lorg/lynxz/smalidemo/ui/activity/HandlerTestActivity;
.end annotation

# `InnerClass` 表明這是一個內(nèi)部類, 而 `name=null` 表示這是匿名內(nèi)部類
.annotation system Ldalvik/annotation/InnerClass;
    accessFlags = 0x0
    name = null
.end annotation

# `synthetic` 表明這是由編譯器自動生成的成員變量
# 通過此處我們知道了, 本 `Handler` 子類強引用了 `Activity`,并將其設置為了成員變量
# instance fields
.field final synthetic this$0:Lorg/lynxz/smalidemo/ui/activity/HandlerTestActivity;

# direct methods
.method constructor <init>(Lorg/lynxz/smalidemo/ui/activity/HandlerTestActivity;)V
    .locals 0

    # 使用寄存器 p1 表示傳遞進來的方法參數(shù) `this$0`, 它是 `HandlerTestActivity` 對象
    .param p1, "this$0"    # Lorg/lynxz/smalidemo/ui/activity/HandlerTestActivity;

    # 將形參 this$0 賦值給本類成員變量 this$0,即:
    # this.this$0=this$0
    .line 26
    iput-object p1, p0, Lorg/lynxz/smalidemo/ui/activity/HandlerTestActivity$1;->this$0:Lorg/lynxz/smalidemo/ui/activity/HandlerTestActivity;

    invoke-direct {p0}, Landroid/os/Handler;-><init>()V

    return-void
.end method

以上很明確的說明了: 非靜態(tài)內(nèi)部類會持有外部類的引用,且是強引用;

P.S. 上面的代碼是匿名內(nèi)部類,對于具名內(nèi)部類也是一樣的結(jié)果;

3. 靜態(tài)內(nèi)部類是否也會持有外部類的引用呢?

我們再定義一個靜態(tài)內(nèi)部類,看下其smali源碼:

// HandlerTestActivity.java
public class HandlerTestActivity extends AppCompatActivity {

    static class MyEmptyStaticHandler extends Handler {
        @Override
        public void handleMessage(Message msg) {
            super.handleMessage(msg);
        }
    }
}
# HandlerTestActivity.smali
.class public Lorg/lynxz/smalidemo/ui/activity/HandlerTestActivity;
.super Landroid/support/v7/app/AppCompatActivity;
.source "HandlerTestActivity.java"

# 定義了內(nèi)部類列表
# annotations
.annotation system Ldalvik/annotation/MemberClasses;
    value = {
        Lorg/lynxz/smalidemo/ui/activity/HandlerTestActivity$MyEmptyStaticHandler;
    }
.end annotation

# 聲明成員變量 `myEmptyStaticHandler`
# instance fields
.field private myEmptyStaticHandler:Lorg/lynxz/smalidemo/ui/activity/HandlerTestActivity$MyEmptyStaticHandler;

# direct methods
.method public constructor <init>()V
    .locals 1
    .line 20
    invoke-direct {p0}, Landroid/support/v7/app/AppCompatActivity;-><init>()V

    .line 35
    new-instance v0, Lorg/lynxz/smalidemo/ui/activity/HandlerTestActivity$MyEmptyStaticHandler;

    # 可以發(fā)現(xiàn)此處并未把 `HandlerTestActivity` 對象作為參數(shù)傳遞到 `init()` 方法中
    invoke-direct {v0}, Lorg/lynxz/smalidemo/ui/activity/HandlerTestActivity$MyEmptyStaticHandler;-><init>()V

    iput-object v0, p0, Lorg/lynxz/smalidemo/ui/activity/HandlerTestActivity;->myEmptyStaticHandler:Lorg/lynxz/smalidemo/ui/activity/HandlerTestActivity$MyEmptyStaticHandler;

    return-void
.end method

由以上代碼即可知: 靜態(tài)內(nèi)部類并不會持有外部類的引用;

這就解釋了AS給出的優(yōu)化建議的第一條;

4. 為何使用 WeakReference

我們通常都需要在 Handler 的消息處理邏輯中去操作 Activity,如更新UI等,因此它還是需要持有 Activity 的引用,但同時又不能阻礙 GC 的回收操作;

自然而然就想到 WeakReference ,關(guān)于 Java 的四種引用此處不展開;

// HandlerTestActivity.java
public class HandlerTestActivity extends AppCompatActivity {
    private String pName;
    private String pName1;
    private static String sName;
    private static String sName1;

    // 編譯器會自動生成一個與外部類處于相同package下的內(nèi)部類: `HandlerTestActivity$MyStaticHandler.smali`
    private static class MyStaticHandler extends Handler {
        private final WeakReference<HandlerTestActivity> mWkActivity;

        public MyStaticHandler(HandlerTestActivity activity) {
            mWkActivity = new WeakReference<HandlerTestActivity>(activity);
        }

        public Activity getActivity() {
            return mWkActivity.get();
        }

        @Override
        public void handleMessage(Message msg) {
            super.handleMessage(msg);
            HandlerTestActivity targetAct = mWkActivity.get();
            // 通過 `WeakReference` 對象去操作外部 `Activity` 屬性和事件
            if (targetAct != null && !targetAct.isFinishing()) {
                String name = targetAct.pName; // 訪問外部類private屬性
                String sName = HandlerTestActivity.sName;
                targetAct.callPrivateFunc(); // 調(diào)用外部類private的方法
                targetAct.pName = ""; // 設置外部類private屬性的值
            }
        }
    }
}

看一下生成的smali類文件結(jié)構(gòu):

?  Desktop cd app-debug/smali/org/lynxz/smalidemo/ui
?  ui tree
.
└── activity
    ├── HandlerTestActivity$1.smali # 匿名內(nèi)部類
    ├── HandlerTestActivity$MyStaticHandler.smali # 具名內(nèi)部類
    └── HandlerTestActivity.smali # 外部類smali

5. 為何內(nèi)部類可以訪問外部類的所有方法和變量,包括 private

AS 給出的優(yōu)化提示第三條: 通過持有的外部類對象去操作或訪問外部類的所有方法和變量;

此處就產(chǎn)生了一個疑問:

Java 四種訪問權(quán)限: public/protect/default/private , 既然編譯器會自動生成一個同package下的內(nèi)部類,為何其仍可以訪問外部類的private參數(shù)和方法呢?

看下 MyStaticHandler 源碼:

# HandlerTestActivity$MyStaticHandler.smali
# instance fields
.field private final mWkActivity:Ljava/lang/ref/WeakReference;
    .annotation system Ldalvik/annotation/Signature;
        value = {
            "Ljava/lang/ref/WeakReference<",
            "Lorg/lynxz/smalidemo/ui/activity/HandlerTestActivity;",
            ">;"
        }
    .end annotation
.end field

.method public handleMessage(Landroid/os/Message;)V
    .locals 4

    # 使用寄存器 p1 表示方法形參 `msg` 的值
    .param p1, "msg"    # Landroid/os/Message;

    .line 57
    invoke-super {p0, p1}, Landroid/os/Handler;->handleMessage(Landroid/os/Message;)V

    # 獲取成員變量 WeakRefrence 所持有的 `HandlerTestActivity` 對象,并定義為局部變量 targetAct,賦值給 v0 寄存器
    # 對應Java源碼: `HandlerTestActivity targetAct = mWkActivity.get();`
    .line 58
    iget-object v0, p0, Lorg/lynxz/smalidemo/ui/activity/HandlerTestActivity$MyStaticHandler;->mWkActivity:Ljava/lang/ref/WeakReference;
    invoke-virtual {v0}, Ljava/lang/ref/WeakReference;->get()Ljava/lang/Object;
    move-result-object v0
    check-cast v0, Lorg/lynxz/smalidemo/ui/activity/HandlerTestActivity;
    .line 59
    .local v0, "targetAct":Lorg/lynxz/smalidemo/ui/activity/HandlerTestActivity;

    # 若該對象為null,則跳轉(zhuǎn)到標簽 cond_0 處繼續(xù)執(zhí)行
    if-eqz v0, :cond_0

    # 獲取 `activity.isFinishing()` 值并賦值給v1寄存器
    # 若 v1 == true ,則跳轉(zhuǎn)到的標簽 `cond_0` 定義處繼續(xù)執(zhí)行
    invoke-virtual {v0}, Lorg/lynxz/smalidemo/ui/activity/HandlerTestActivity;->isFinishing()Z
    move-result v1
    if-nez v1, :cond_0

    # 此處調(diào)用 `HandlerTestActivity` 的靜態(tài)方法 `access$000()` 并返回一個 `String` 值,并值賦值給 v1,而 v1 表示局部變量 name
    # 因此對應于Java源碼: `String name = targetAct.pName;`
    .line 60
    invoke-static {v0}, Lorg/lynxz/smalidemo/ui/activity/HandlerTestActivity;->access$000(Lorg/lynxz/smalidemo/ui/activity/HandlerTestActivity;)Ljava/lang/String;
    move-result-object v1
    .line 61
    .local v1, "name":Ljava/lang/String; # 用 v1 寄存器表示局部變量 name

    # 對應Java源碼: `String sName = HandlerTestActivity.access$100()`
    invoke-static {}, Lorg/lynxz/smalidemo/ui/activity/HandlerTestActivity;->access$100()Ljava/lang/String;
    move-result-object v2
    .line 62
    .local v2, "sName":Ljava/lang/String;

    # 對應Java源碼: `targetAct.callPrivateFunc();`
    invoke-static {v0}, Lorg/lynxz/smalidemo/ui/activity/HandlerTestActivity;->access$200(Lorg/lynxz/smalidemo/ui/activity/HandlerTestActivity;)V

    # 對應Java源碼: `targetAct.pName = "";`
    .line 63
    const-string v3, ""
    invoke-static {v0, v3}, Lorg/lynxz/smalidemo/ui/activity/HandlerTestActivity;->access$002(Lorg/lynxz/smalidemo/ui/activity/HandlerTestActivity;Ljava/lang/String;)Ljava/lang/String;

    .line 65
    .end local v1    # "name":Ljava/lang/String;
    .end local v2    # "sName":Ljava/lang/String;
    :cond_0
    return-void
.end method

以上源碼中的 access$100()/access$200() 等方法并不是我們定義的,通過其命名方式也能知曉這是編譯器生成的,我們看下他們是做什么用的:

# HandlerTestActivity.smali
# `synthetic` 表明這是編譯器自動生成的方法, package訪問權(quán)限的靜態(tài)方法
# 用于訪問實例的私有成員變量 pName
.method static synthetic access$002(Lorg/lynxz/smalidemo/ui/activity/HandlerTestActivity;Ljava/lang/String;)Ljava/lang/String;
    .locals 0
    .param p0, "x0"    # Lorg/lynxz/smalidemo/ui/activity/HandlerTestActivity;
    .param p1, "x1"    # Ljava/lang/String;
    .line 20
    iput-object p1, p0, Lorg/lynxz/smalidemo/ui/activity/HandlerTestActivity;->pName:Ljava/lang/String;
    return-object p1
.end method

# 編譯器自動生成的靜態(tài)方法,用于類的私有成員變量 sName
.method static synthetic access$100()Ljava/lang/String;
    .locals 1
    .line 20
    sget-object v0, Lorg/lynxz/smalidemo/ui/activity/HandlerTestActivity;->sName:Ljava/lang/String;
    return-object v0
.end method

# 編譯器自動生成的靜態(tài)方法,用于訪問實例的私有方法 callPrivateFunc
.method static synthetic access$200(Lorg/lynxz/smalidemo/ui/activity/HandlerTestActivity;)V
    .locals 0
    .param p0, "x0"    # Lorg/lynxz/smalidemo/ui/activity/HandlerTestActivity;
    .line 20
    invoke-direct {p0}, Lorg/lynxz/smalidemo/ui/activity/HandlerTestActivity;->callPrivateFunc()V
    return-void
.end method

由此我們知道了: 若編譯器發(fā)現(xiàn)內(nèi)部類需要訪問外部類的私有屬性或方法,則會自動生成一個對應包訪問權(quán)限的靜態(tài)方法,間接調(diào)用;

6. 小結(jié)

  1. 非靜態(tài)內(nèi)部類會持有外部類的強引用;
  2. 靜態(tài)內(nèi)部類默認不會持有外部類的引用;
  3. 通過 WeakReference, 可以實現(xiàn)既能訪問外部類的成員,又不影響GC;
  4. 編譯器會按需自動生成一些方法/屬性,用于內(nèi)部類進行訪問的同時又不會違反訪問權(quán)限的要求;
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市雀鹃,隨后出現(xiàn)的幾起案子息楔,更是在濱河造成了極大的恐慌,老刑警劉巖麻车,帶你破解...
    沈念sama閱讀 216,324評論 6 498
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件伯铣,死亡現(xiàn)場離奇詭異崎坊,居然都是意外死亡阵苇,警方通過查閱死者的電腦和手機壁公,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,356評論 3 392
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來绅项,“玉大人紊册,你說我怎么就攤上這事】旃ⅲ” “怎么了囊陡?”我有些...
    開封第一講書人閱讀 162,328評論 0 353
  • 文/不壞的土叔 我叫張陵,是天一觀的道長掀亥。 經(jīng)常有香客問我撞反,道長,這世上最難降的妖魔是什么搪花? 我笑而不...
    開封第一講書人閱讀 58,147評論 1 292
  • 正文 為了忘掉前任痢畜,我火速辦了婚禮垛膝,結(jié)果婚禮上鳍侣,老公的妹妹穿的比我還像新娘丁稀。我一直安慰自己,他們只是感情好倚聚,可當我...
    茶點故事閱讀 67,160評論 6 388
  • 文/花漫 我一把揭開白布线衫。 她就那樣靜靜地躺著,像睡著了一般惑折。 火紅的嫁衣襯著肌膚如雪授账。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,115評論 1 296
  • 那天惨驶,我揣著相機與錄音白热,去河邊找鬼。 笑死粗卜,一個胖子當著我的面吹牛屋确,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播续扔,決...
    沈念sama閱讀 40,025評論 3 417
  • 文/蒼蘭香墨 我猛地睜開眼攻臀,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了纱昧?” 一聲冷哼從身側(cè)響起刨啸,我...
    開封第一講書人閱讀 38,867評論 0 274
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎识脆,沒想到半個月后设联,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,307評論 1 310
  • 正文 獨居荒郊野嶺守林人離奇死亡灼捂,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,528評論 2 332
  • 正文 我和宋清朗相戀三年离例,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片纵东。...
    茶點故事閱讀 39,688評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡粘招,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出偎球,到底是詐尸還是另有隱情洒扎,我是刑警寧澤,帶...
    沈念sama閱讀 35,409評論 5 343
  • 正文 年R本政府宣布衰絮,位于F島的核電站袍冷,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏猫牡。R本人自食惡果不足惜胡诗,卻給世界環(huán)境...
    茶點故事閱讀 41,001評論 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧煌恢,春花似錦骇陈、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,657評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至二汛,卻和暖如春婿崭,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背肴颊。 一陣腳步聲響...
    開封第一講書人閱讀 32,811評論 1 268
  • 我被黑心中介騙來泰國打工氓栈, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人婿着。 一個月前我還...
    沈念sama閱讀 47,685評論 2 368
  • 正文 我出身青樓授瘦,卻偏偏與公主長得像,于是被迫代替她去往敵國和親祟身。 傳聞我的和親對象是個殘疾皇子奥务,可洞房花燭夜當晚...
    茶點故事閱讀 44,573評論 2 353

推薦閱讀更多精彩內(nèi)容