Trước khi tôi bắt đầu, mã của bài viết này có sẵn tại kotlin-mem-leak của repo performance-test:
https://github.com/marcosholgado/performance-test/tree/kotlin-mem-leak
Toàn bộ tiền đề rất đơn giản, tôi muốn viết một Activity sẽ bị memory leak để tôi có thể phát hiện ra điều đó trong integration test. Vì tôi đã sử dụng công cụ rò rỉ nên tôi đã sao chép Hoạt động mẫu của họ để tạo lại rò rỉ bộ nhớ. Tôi đã xóa một số mã từ mẫu và kết thúc với lớp Java sau.
public class LeakActivity extends Activity {
@Override protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_leak);
View button = findViewById(R.id.button);
button.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
startAsyncWork();
}
});
}
@SuppressLint("StaticFieldLeak")
void startAsyncWork() {
Runnable work = new Runnable() {
@Override public void run() {
SystemClock.sleep(20000);
}
};
new Thread(work).start();
}
}
LeakActivity có một nút mà khi nhấn, sẽ tạo ra một Runnable mới chạy
trong 20 giây. Vì Runnable là một lớp ẩn danh, nó có một tham chiếu ẩn
đến LeakActivity bên ngoài và nếu Activity bị hủy trước khi luồng kết
thúc (20 giây sau khi nhấn nút) thì Activity sẽ bị rò rỉ. Nó sẽ không bị
rò rỉ mãi mãi, sau 20 giây đó, nó sẽ sãn sàng để được thu thập lại.Vì tôi đang viết mã của mình trong Kotlin, tôi đã chuyển đổi lớp Java đó thành mã Kotlin trông như thế này:
class KLeakActivity : Activity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_leak)
button.setOnClickListener { startAsyncWork() }
}
private fun startAsyncWork() {
val work = Runnable { SystemClock.sleep(20000) }
Thread(work).start()
}
}
Không có gì đặc biệt ở đây, tôi đang tận dụng lambdas để loại bỏ
boilerplate khỏi Runnable và trên lý thuyết mọi thứ sẽ giống nhau phải
không? Sau đó tôi test bằng cách sử dụng công cụ rò rỉ và chú thích @LeakTest của riêng tôi để chạy bộ phân tích bộ nhớ của họ chỉ trong bài kiểm tra này.class LeakTest {
@get:Rule
var mainActivityActivityTestRule = ActivityTestRule(KLeakActivity::class.java)
@Test
@LeakTest
fun testLeaks() {
onView(withId(R.id.button)).perform(click())
}
}
Thử nghiệm thực hiện một lần bấm nút và vì đó là điều duy nhất chúng
tôi đang thực hiện, hoạt động sẽ bị hủy ngay lập tức sau đó và tạo ra rò
rỉ vì chúng tôi không đợi trong 20 giây.Nếu chúng tôi cố gắng thực hiện kiểm tra testLeaks bên trong MyKLeakTest bạn sẽ thấy các kiểm tra được cho qua có nghĩa là chúng tôi đã không phát hiện bất kỳ rò rỉ bộ nhớ nào.
Kết quả này làm tôi bối rối rất nhiều, vào cuối ngày tôi đã thay thế lớp Java ẩn danh đó bằng một lớp bên trong Ẩn danh và vì đó là một instance của functional Java interface, tôi có thể sử dụng biểu thức lambda thay thế (thêm về các phương thức trừu tượng đơn lẻ hoặc Chuyển đổi SAM ở đây ).
Tôi đã viết một hoạt động mới, cùng mã nhưng lần này tôi giữ nó trong Java. Tôi đã thay đổi bài kiểm tra để trỏ đến activity mới này, chạy nó và lần này, nó đã thất bại. Mọi thứ bắt đầu có ý nghĩa hơn một chút bây giờ. Mã Kotlin phải khác với mã Java, một cái gì đó đã xảy ra ở đó và chỉ có một nơi để tìm nó: Byte code .
Phân tích LeakActivity.java Để bắt đầu, tôi đã phân tích Mã số Dalvik của hoạt động Java. Để làm điều đó, bạn phải phân tích apk của mình thông qua Build/Analyze APK... và sau đó chọn từ tệp classes.dex mà lớp bạn muốn phân tích.

Chúng tôi nhấp chuột phải vào lớp và chọn Show Bytecode để lấy Dalvi Bytecode của lớp. Tôi sẽ chỉ tập trung vào phương thức startAsyncWork vì chúng tôi biết đó là nơi xảy ra rò rỉ bộ nhớ.
.method startAsyncWork()V
.registers 3
.annotation build Landroid/annotation/SuppressLint;
value = {
"StaticFieldLeak"
}
.end annotation
.line 29
new-instance v0, Lcom/marcosholgado/performancetest/LeakActivity$2;
invoke-direct {v0, p0}, Lcom/marcosholgado/performancetest/LeakActivity$2;-><init>
(Lcom/marcosholgado/performancetest/LeakActivity;)V
.line 34
.local v0, "work":Ljava/lang/Runnable;
new-instance v1, Ljava/lang/Thread;
invoke-direct {v1, v0}, Ljava/lang/Thread;-><init>(Ljava/lang/Runnable;)V
invoke-virtual {v1}, Ljava/lang/Thread;->start()V
.line 35
return-void
.end method
Chúng tôi biết rằng một lớp ẩn danh giữ một tham chiếu đến lớp bên
ngoài vì vậy chúng tôi sẽ tìm kiếm điều đó. Trong mã byte ở trên, chúng
tôi tạo một phiên bản mới của LeakActivity$2 và chúng tôi lưu trữ nó
trong v0 (dòng 10).new-instance v0, Lcom/marcosholgado/performancetest/LeakActivity$2;
Nhưng LeakActivity$2 gì? Nếu chúng ta tiếp tục nhìn vào tập tin class.dex của bạn, bạn sẽ tìm thấy nó ở đó.Vì vậy, hãy xem mã byte Dalvik cho lớp đó. Tôi đã xóa một số mã khỏi kết quả mà chúng tôi không thực sự quan tâm.
.class Lcom/marcosholgado/performancetest/LeakActivity$2;
.super Ljava/lang/Object;
.source "LeakActivity.java"
# interfaces
.implements Ljava/lang/Runnable;
# instance fields
.field final synthetic this$0:Lcom/marcosholgado/performancetest/LeakActivity;
# direct methods
.method constructor <init>(Lcom/marcosholgado/performancetest/LeakActivity;)V
.registers 2
.param p1, "this$0" # Lcom/marcosholgado/performancetest/LeakActivity;
.line 29
iput-object p1, p0, Lcom/marcosholgado/performancetest/LeakActivity$2;
->this$0:Lcom/marcosholgado/performancetest/LeakActivity;
invoke-direct {p0}, Ljava/lang/Object;-><init>()V
return-void
.end method
Điều thú vị đầu tiên bạn có thể thấy là lớp này triển khai Runnable.# interfaces
.implements Ljava/lang/Runnable;
Giống như tôi đã nói trước lớp này nên có một tài liệu tham khảo cho
lớp bên ngoài vậy nó ở đâu? Ngay bên dưới giao diện có một trường ví dụ
kiểu LeakActivity .# instance fields
.field final synthetic
this$0:Lcom/marcosholgado/performancetest/LeakActivity;
Và nếu chúng ta nhìn vào hàm tạo của Runnable của bạn, bạn sẽ thấy rằng nó cần một tham số, LeakActivity ..method constructor
<init>(Lcom/marcosholgado/performancetest/LeakActivity;)V
Quay trở lại mã byte của LeakActivity nơi chúng tôi đang tạo một
LeakActivity$2 , bạn có thể thấy cách nó sử dụng thể hiện đó (được lưu
trữ trong v0) để gọi hàm tạo mà chúng ta vừa thấy để truyền một thể hiện
của LeakActivity .new-instance v0, Lcom/marcosholgado/performancetest/LeakActivity$2;
invoke-direct {v0, p0},
Lcom/marcosholgado/performancetest/LeakActivity$2;-><init>
(Lcom/marcosholgado/performancetest/LeakActivity;)V
Vì vậy, lớp LeakActivity.java thực sự sẽ bị rò rỉ nếu nó bị kill
trước khi Runnable kết thúc vì nó có tham chiếu đến Activity và nó sẽ
không phải là bộ nhớ rác được thu thập tại thời điểm đó.Analyzing LeakActivity.java còn tiếp...
Comments
Post a Comment