本文基于:
macOS:10.13/AS:3.4/Android build-tools:28.0.0/jdk: 1.8/apktool: 2.3.3
1. Handler內(nèi)存泄露測試
在 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);
}
}
內(nèi)存出現(xiàn)了明顯了升高;
簡單描述下原因:
- 由于上面的
Handler
內(nèi)部類定義在ui線程中,因此使用的主線程的Looper
和MessageQueue
; -
MessageQueue
中的Message
會持有Handler
對象; - 匿名
Handler
內(nèi)部類對象持有著外部Activity
的強引用;
以上三點導致當有 Message
未被處理之前, 外部類 Activity
會一直被強引用,導致即使發(fā)生了銷毀,也無法被GC回收;
因此處理方法通常有兩種:
- 在外部類
Activity
銷毀時取消所有的Message
,即leakHandler.removeCallbacksAndMessages(null);
- 讓內(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é)
- 非靜態(tài)內(nèi)部類會持有外部類的強引用;
- 靜態(tài)內(nèi)部類默認不會持有外部類的引用;
- 通過
WeakReference
, 可以實現(xiàn)既能訪問外部類的成員,又不影響GC; - 編譯器會按需自動生成一些方法/屬性,用于內(nèi)部類進行訪問的同時又不會違反訪問權(quán)限的要求;