標籤:

Android 坑檔案:當 WeakReference 遇上了 Lambda

每一個行業都有自己的坑,所謂經驗多寡無非是有沒有踩過坑。我把 Android 開發中遇到的各種坑彙集起來,於是就有了這部『Android 坑檔案』。

1、背景

最近寫了個的簡單的全局事件庫,功能上有點兒像 EventBus。當時為了偷懶,想著在事件管理器當中註冊一個 Runnable,這樣的話,我註冊事件的寫法就會非常清爽,比如:

EventController.register("my_event_name", new Runnable(){n @Overriden public void run(){ ... }n});n

而我在 EventController 當中又做了什麼呢?我把註冊進來的這個 Runnable 存到了一個 WeakReference 數組裡面,所以結果可想而知,一輪 GC 之後,這個 Runnable 就兩腿兒一蹬上了西~天~。

可是,我也是多事兒,居然隨手用 Kotlin 寫了個 case,發現事情竟然不是我想像的那麼簡單,有了發現當然要拿出來嘚瑟一下,不然也不是我的性格對吧。

前面已經寫過一篇文章介紹 Kotlin,我也已經開始嘗試用 Kotlin 開發一些 demo。Kotlin 有一些比較方便好用的特性,比如 Lambda,

executor.submit{n //reach the internet...n}n

而我們在 Java 當中還要去構造一個匿名內部類,寫多了不僅寫著煩,看著也。。不能愉快啊:

executor.submit(new Runnable(){nn @Overriden public void run(){n //reach the internet...n }nn});n

Lambda 目前在 C++ 11 和 Java 8 都已經陸續被支持,可見這個東西確實是有它的過人之處啊。

再來說說我們今天的另一位主人公 WeakReference。我們知道 Java 對象的內存回收是依賴 GC 的,如果我們對某一個對象有強引用,這個對象的回收就會受到影響;而如果我們既希望能夠拿到這個對象,同時也不希望影響它的內存回收,我們就可以用 WeakReference 了,就像下面這樣:

WeakReference<Foo> fooRef = new WeakReference<>(foo);n//if(fooRef.get() != null){n//do something heren//}n

一句話,如果一個對象被弱引用了,那麼它的生命周期不會被影響,只要遇到一次 GC,它的內存就會被回收。

那麼我究竟發現了什麼呢?您別急,咱慢慢說。

2、相遇是美好的,於是便難捨難分

當 Kotlin 的 Lambda 和 WeakReference 相遇時,事情就變得複雜起來。我當時在使用我的事件框架時,傳入了一個 Lambda 表達式,就像這樣:

EventController.register("my_event_name"){ n Log.d("test", "event triggered") n}n

神奇的事情發生了,不管我 GC 多少次,這個傳入的 Lambda 表達式總是跟成了精一樣依然存活並且能夠響應這個事件。為了證明這確實不是鬧鬼,我又專門寫了個 demo 來複現這個問題,企圖查明真相。

Dummy.java

public class Dummy{n public long a = 10;n public double b = 10.0;n}n

這個類什麼邏輯都沒有,只是用來觸發 GC 的。

Holder.java

import java.lang.ref.WeakReference;nimport java.util.ArrayList;nnpublic class Holder {n private static ArrayList<WeakReference<Object>> refList = new ArrayList<>();nn public static void dump(){n for (WeakReference<Object> objectWeakReference : refList) {n if(objectWeakReference == null) System.out.println("null....");n else System.out.println(objectWeakReference.get());n }n }nn public static void add(Runnable obj){n refList.add(new WeakReference<Object>(obj));n }n}n

這個類主要是用來存儲弱引用的。

下面先給出第一個版本的 HelloInKotlin.kt:

class Hello(){n init{n Holder.add({n println("hello")n })n }n}nnfun main(args: Array<String>) {n var hello: Hello? = Hello()n Holder.dump()n for(i in 1 .. 100000){n Dummy()n }n System.gc()n Holder.dump()n}n

代碼非常簡單,我們看到在 Hello這個類被實例化時,會往 Holder 的靜態 ArrayList 當中添加一個 Lambda 表達式對象的弱引用,第一個 "Holder.dump()" 輸出的自然是 這個弱引用,沒有問題的。

好,我們現在要 GC 了——創建 10w 個 Dummy 對象只是為了告訴虛擬機你確實應該清理一下垃圾了——緊接著又 dump,我想,結果應該就什麼都沒了,也就是輸出 "null",然而結果確是:

com.benny.Hello$1@5305068ancom.benny.Hello$1@5305068an

額。。難道,是我的用法不對??或者,這就是真相?於是我想著再用 Java 把 Hello 的代碼實現一下看看:

public class HelloInJava {nn public HelloInJava() {n Holder.add(new Runnable() {n @Overriden public void run() {n System.out.println("Hello there...");n }n });n }nn public static void main(String ... args){n HelloInJava helloInJava = new HelloInJava();n Holder.dump();n for(int i = 0; i < 100000; i++){n new Dummy();n }n System.gc();nn Holder.dump();n }n}n

代碼幾乎一樣,這回又要輸出什麼呢?

com.benny.HelloInJava$1@3fee733dnnulln

啊哈,WeakReference 對於 Lambda 對象是失效了么?我當然不甘心,於是又祭出了下面的招數:

class Hello(){n init{n Holder.add(object :Runnable{n override fun run() {n println("hello")n }n })n }n}nnfun main(args: Array<String>) {n var hello: Hello? = Hello()n Holder.dump()n for(i in 1 .. 100000){n Dummy()n }n System.gc()n Holder.dump()n}n

我們把 Hello 的構造做了一些調整,原來的 Lambda 被顯式的用匿名內部類來替代,在 Kotlin 當中,我很少想過它們會有什麼區別,直到這一刻:

com.benny.Hello$1@5305068annulln

天哪,居然跟 Java 的匿名內部類方式的結果一樣。。

在這一刻,我們其實並不能直到上面這些代碼究竟隱藏著怎樣的秘密,不過我們可以直到的是,WeakReference 居然會影響 Lambda 表達式的生命周期!!

3、如膠似漆的背後究竟隱藏著什麼

看過我的文章的朋友應該熟悉我的風格,遇到問題就死纏爛打,並始終給大家講述了一個『挖掘機技術哪家強』的故事。可是,這次,代碼就那麼點兒,還是我們自己寫出來的,這回該往哪兒挖呢??

為了搞清楚我們寫的代碼究竟是怎麼工作的,我決定查看一下它們對應的位元組碼,前面提到的 Lambda 和匿名內部類的位元組碼如下:

Lambda 表達式生成的位元組碼

L1n LINENUMBER 13 L1n GETSTATIC com/benny/Hello$1.INSTANCE : Lcom/benny/Hello$1;n CHECKCAST java/lang/Runnablen INVOKESTATIC com/oracle/demo/Holder.add (Ljava/lang/Runnable;)Vn

也就是說,這段位元組碼對應的是下面的 Kotlin 代碼:

Holder.add({n println("hello")n })n

匿名內部類生成的位元組碼

L2n LINENUMBER 17 L2n NEW com/benny/Hello$2n DUPn INVOKESPECIAL com/benny/Hello$2.<init> ()Vn CHECKCAST java/lang/Runnablen INVOKESTATIC com/oracle/demo/Holder.add (Ljava/lang/Runnable;)Vn

對應於:

Holder.add(object :Runnable{n override fun run() {n println("hello")n }n })n

仔細觀察位元組碼 L1 後面有一句 "GETSTATIC com/benny/Hello$1.INSTANCE : Lcom/benny/Hello$1;",不知道你會不會想到什麼:

public class Singleton{n private static Singleton INSTANCE;n ......n}n

單例??就是說 Lambda 表達式其實是做了個單例出來?我們再來看下匿名內部類的位元組碼:

NEW com/benny/Hello$2n DUPn INVOKESPECIAL com/benny/Hello$2.<init> ()Vn

第一句實際上就是開闢內存,第三句則是調用構造方法——就是一個普通的類實例化過程。這下可有意思了,我們甚至可以通過反射來拿到這個所謂的 "INSTANCE":

fun main(args: Array<String>) {n try {n val clazz = Class.forName("com.benny.Hello$1")n var f = clazz.getField("INSTANCE");n println(f.get(null))n f = nulln }catch(e: Exception){n e.printStackTrace()n }n}n

輸出

com.benny.Hello$1@54bedef2n

嚯,這有意思了啊,貨真價實的單例啊,而且這個東西完全不依賴與 Hello 這個類的對象的生命周期。

4、會內存泄露嗎?

一旦發現這個,我們就肯定會思考一個關乎存亡興替的大問題:這裡的 Lambda 表達式會不會引發內存泄露?如果 Lambda 內部引用了外部對象的成員,而它自己又釋放不了,外部對象豈不是眼巴巴的看著自己已然風燭殘年卻沒辦法投胎轉世從而獲得新生??

真是細思極恐啊。

為了一探究竟,還是用剛才的 demo 稍稍做了點兒修改:

class Hello(){n var anInt: Int = 2n n init{n Holder.add({n println("hello: ${anInt}")n })n }n}nnfun main(args: Array<String>) {n var hello: Hello? = Hello()n Holder.dump()n for(i in 1 .. 100000){n Dummy()n }n System.gc()n Holder.dump()n}n

注意到我在 Lambda 中引用了外部對象的成員 "anInt",其他都沒有變化,想著先運行一下看看,結果卻更有意思了:

com.benny.Hello$1@5305068annulln

哎呀,難道是這傢伙發現我要整他,偷偷跑了??我幾乎不敢相信自己的眼睛,於是多運行了幾次,結果仍然是這樣。哈,想來,造物者是對的,這個時候如果這個 Lambda 一直老不死的,那麼我們構造出來的那個 hello 對象可就怨念太深了。

我只是讓 Lambda 引用了一個外部對象的成員啊,這究竟意味著什麼呢?

L2n LINENUMBER 14 L2n NEW com/benny/Hello$1n DUPn ALOAD 0n INVOKESPECIAL com/benny/Hello$1.<init> (Lcom/benny/Hello;)Vn CHECKCAST java/lang/Runnablen INVOKESTATIC com/oracle/demo/Holder.add (Ljava/lang/Runnable;)Vn RETURNn

我們發現,這時候 Lambda 表達式對應的位元組碼不再是 INSTANCE,而是跟匿名內部類一樣的

NEW com/benny/Hello$1n

換句話說,Lambda 表達式的位元組碼並不是一成不變的,在引用了外部對象的成員之後,其行為與匿名內部類相似;否則,就是以 INSTANCE 的形式存在了。

5、小結

討論了這麼久,其實有幾個問題是需要搞清楚的:

  1. 為什麼在純代碼的情況下是單例?

    實際上對於方法的代碼段,如果它是獨立的,不受其他對象約束的(也就是沒有引用到其他對象的成員),那麼這段代碼其實就是可復用的。通常情況下,編譯器還可以對這樣的代碼做內聯,甚至復用——它看上去也許還有點兒像字元串字面量,對於它們的回收(如果有的話),編譯器肯定有著另一套打算。

  2. 為什麼在引用了外部作用域的成員後變成了與匿名內部類相同的實現?

    一旦它們不再是獨立的,這些 Lambda 就會被退化成普通的對象,是不是有一種神仙剔去仙骨的即視感——他的生命需要遵守凡人的準則了。

  3. 該不該用 Lambda?

    當然是該啊,我們前面討論了這麼久,其實並沒有發現 Lambda 會導致什麼問題(或者說,它有的問題匿名內部類都有)。不過對於外部對象無關獨立代碼段,我們也應該是有所了解的,不然就會遇到我在文章一開始遇到的問題——嗯,如果你不知道什麼原因,你是不是會開始相信冥冥之中自然天數?

當然,如果大家有興趣,也可以嘗試使用 Java 8 的 Lambda 做類似的實驗,你會發現其行為與 Kotlin 一致。


推薦閱讀:

TAG:Kotlin |