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

掘金博客链接

相关demo源码;

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

1. Handler内存泄露测试

IDE提示

Activity 中创建 Handler 内部类时,AS会给提内存泄露提示及解决方案:

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 监控内存变化:

// 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);
    }
}
profiler内存监控

内存出现了明显了升高;

简单描述下原因:

  1. 由于上面的 Handler 内部类定义在ui线程中,因此使用的主线程的 LooperMessageQueue;
  2. MessageQueue 中的 Message 会持有 Handler 对象;
  3. 匿名Handler内部类对象持有着外部 Activity 的强引用;

以上三点导致当有 Message 未被处理之前, 外部类 Activity 会一直被强引用,导致即使发生了销毁,也无法被GC回收;

因此处理方法通常有两种:

  1. 在外部类 Activity 销毁时取消所有的 Message,即 leakHandler.removeCallbacksAndMessages(null);
  2. 让内部类不要持有外部Activity的强引用;

AS给出的提示方案属于第二种, 我们通过smali源码来一步步探究验证下;

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. 编译器会按需自动生成一些方法/属性,用于内部类进行访问的同时又不会违反访问权限的要求;

推荐阅读更多精彩内容