博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
[smali] This Handler class should be static or leaks might occur
阅读量:6592 次
发布时间:2019-06-24

本文共 12113 字,大约阅读时间需要 40 分钟。

;

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

1. Handler内存泄露测试

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.javapublic 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);    }}复制代码

内存出现了明显了升高;

简单描述下原因:

  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 
()V .locals 1 .line 20 invoke-direct {p0}, Landroid/support/v7/app/AppCompatActivity;->
()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;->
(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 
(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;->
()V return-void.end method复制代码

以上很明确的说明了: 非静态内部类会持有外部类的引用,且是强引用;

P.S. 上面的代码是匿名内部类,对于具名内部类也是一样的结果;

3. 静态内部类是否也会持有外部类的引用呢?

我们再定义一个静态内部类,看下其smali源码:

// HandlerTestActivity.javapublic 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 
()V .locals 1 .line 20 invoke-direct {p0}, Landroid/support/v7/app/AppCompatActivity;->
()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;->
()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.javapublic 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
mWkActivity; public MyStaticHandler(HandlerTestActivity activity) { mWkActivity = new WeakReference
(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. 编译器会按需自动生成一些方法/属性,用于内部类进行访问的同时又不会违反访问权限的要求;

转载于:https://juejin.im/post/5cbbceef6fb9a068a4118cf3

你可能感兴趣的文章
Linux设备模型 (4)
查看>>
iOS报错:linker command failed with exit code 1 (use -v to see invocation)
查看>>
App Store付费应用退款流程(2015超级详细版)
查看>>
事件触发的模型
查看>>
CSS规范 > 9 视觉格式化模型 Visual Formatting Model
查看>>
Angular的模板与路由功能
查看>>
istio-0.8 指标监控,prometheus,grafana
查看>>
支付系统的防重设计
查看>>
Eclipse_常用技巧_01_自动添加类注释和方法注释
查看>>
詹嵩:性能牛逼的Log4j2是个什么鬼?
查看>>
WebView深度学习(三)之WebView的内存泄漏、漏洞以及缓存机制原理和解决方案...
查看>>
《Kotlin 程序设计》第五章 Kotlin 面向对象编程(OOP)
查看>>
[Hadoop]大量小文件问题及解决方案
查看>>
使用 WebStorm 创建 React App
查看>>
光纤通信简史
查看>>
SpringMVC详细教程:教你细节与走位(●'◡'●)
查看>>
LeetCode 326 Power of Three(3的幂)(递归、Log函数)
查看>>
MacOS High Sierra 设置中找不到允许任何来源的安装选项
查看>>
3亿人都参加的95公益周来了,爱心攻略看这里!
查看>>
(码友推荐)2018-08-21 .NET及相关开发资讯速递
查看>>