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

相關demo源碼;java

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

1. Handler內存泄露測試

AS提示

Activity 中建立 Handler 內部類時,AS會給提內存泄露提示及解決方案:git

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 監控內存變化:github

// HandlerTestActivity.java
public class HandlerTestActivity extends AppCompatActivity {

    // 建立匿名Handler內部類的對象
    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 延遲執行,內存泄露測試");
            }
        }, 5 * 60 * 1000);
    }
}
複製代碼

內存泄露

內存出現了明顯了升高;shell

簡單描述下緣由:app

  1. 因爲上面的 Handler 內部類定義在ui線程中,所以使用的主線程的 LooperMessageQueue;
  2. MessageQueue 中的 Message 會持有 Handler 對象;
  3. 匿名Handler內部類對象持有着外部 Activity 的強引用;

以上三點致使當有 Message 未被處理以前, 外部類 Activity 會一直被強引用,致使即便發生了銷燬,也沒法被GC回收;ide

所以處理方法一般有兩種:oop

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

AS給出的提示方案屬於第二種, 咱們經過smali源碼來一步步探究驗證下;post

2. 非靜態內部類持有外部類的強引用

上面的 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` 是匿名內部類, 此處建立了該類的一個對象,並將其賦值給 v0 寄存器
 .line 26
    new-instance v0, Lorg/lynxz/smalidemo/ui/activity/HandlerTestActivity$1;

    # p0 表示 `HandlerTestActivity` 對象自身
    # 此處表示調用 `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` 的子類
    # 結合上一句代碼,咱們就知道 `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` 代表這是一個內部類, 而 `name=null` 表示這是匿名內部類
.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 表示傳遞進來的方法參數 `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
複製代碼

以上很明確的說明了: 非靜態內部類會持有外部類的引用,且是強引用;

P.S. 上面的代碼是匿名內部類,對於具名內部類也是同樣的結果;

3. 靜態內部類是否也會持有外部類的引用呢?

咱們再定義一個靜態內部類,看下其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"

# 定義了內部類列表
# 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;

    # 能夠發現此處並未把 `HandlerTestActivity` 對象做爲參數傳遞到 `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
複製代碼

由以上代碼便可知: 靜態內部類並不會持有外部類的引用;

這就解釋了AS給出的優化建議的第一條;

4. 爲什麼使用 WeakReference

咱們一般都須要在 Handler 的消息處理邏輯中去操做 Activity,如更新UI等,所以它仍是須要持有 Activity 的引用,但同時又不能阻礙 GC 的回收操做;

天然而然就想到 WeakReference ,關於 Java 的四種引用此處不展開;

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

    // 編譯器會自動生成一個與外部類處於相同package下的內部類: `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(); // 調用外部類private的方法
                targetAct.pName = ""; // 設置外部類private屬性的值
            }
        }
    }
}
複製代碼

看一下生成的smali類文件結構:

➜  Desktop cd app-debug/smali/org/lynxz/smalidemo/ui
➜  ui tree
.
└── activity
    ├── HandlerTestActivity$1.smali # 匿名內部類
    ├── HandlerTestActivity$MyStaticHandler.smali # 具名內部類
    └── HandlerTestActivity.smali # 外部類smali
複製代碼

5. 爲什麼內部類能夠訪問外部類的全部方法和變量,包括 private

AS 給出的優化提示第三條: 經過持有的外部類對象去操做或訪問外部類的全部方法和變量;

此處就產生了一個疑問:

Java 四種訪問權限: public/protect/default/private , 既然編譯器會自動生成一個同package下的內部類,爲什麼其仍能夠訪問外部類的private參數和方法呢?

看下 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,則跳轉到標籤 cond_0 處繼續執行
    if-eqz v0, :cond_0

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

    # 此處調用 `HandlerTestActivity` 的靜態方法 `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訪問權限的靜態方法
# 用於訪問實例的私有成員變量 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

# 編譯器自動生成的靜態方法,用於類的私有成員變量 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

# 編譯器自動生成的靜態方法,用於訪問實例的私有方法 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
複製代碼

由此咱們知道了: 若編譯器發現內部類須要訪問外部類的私有屬性或方法,則會自動生成一個對應包訪問權限的靜態方法,間接調用;

6. 小結

  1. 非靜態內部類會持有外部類的強引用;
  2. 靜態內部類默認不會持有外部類的引用;
  3. 經過 WeakReference, 能夠實現既能訪問外部類的成員,又不影響GC;
  4. 編譯器會按需自動生成一些方法/屬性,用於內部類進行訪問的同時又不會違反訪問權限的要求;
相關文章
相關標籤/搜索