
前因
咳咳~ 先說下怎么產(chǎn)生這次的靈異事件,當時的場景是ViewPager中有一個ImageView全屏加載圖片,外加一個ProgressBar在圖片沒有加載完成顯示。
界面是醬嬸的

代碼中大概流程就是當我滑倒第一頁的時候就加載圖片并且顯示進度條,因為是網(wǎng)絡加載有延遲,所以就在加載圖片成功的回掉里面隱藏進度條。大家可能在想 超級簡單是吧? 我當時也是這么想的。
但是想想這里還有些問題,因為我的進度條是寫在Item里面的,也就是說我的每個ViewPager頁面都有一個ProgressBar對象需要控制它顯示和隱藏。又因為ViewPager是有預加載下一頁的特性,當我一看到頁面就會加載本和下一頁 也就是instantiateItem()方法會執(zhí)行兩次,那么會導致當前instantiateItem()方法中的ProgressBar對象不再是第一個頁面的對象,而是第二個頁面的對象,然后當圖片加載成功,回調(diào)方法執(zhí)行隱藏進度條的時候,其實隱藏的是第二個頁面的進度條,然而你現(xiàn)在卻只是在第一個頁面。如果執(zhí)行代碼的話,就會像我上面的說的那樣,是不行的ProgressBar對象是會變的 ,最起碼我之前是這么想的,但是寫都寫了,執(zhí)行以下試試。執(zhí)行后發(fā)現(xiàn),我去! 好使竟然,每一個頁面都能控制自己的ProgressBar對象,簡直是見了鬼了!! 那好,妹的既然見鬼了,我們就抓鬼 ! 下面寫一下模擬代碼來還原當時的場景!
模擬代碼
public void click(View view) { for (int i = 0; i < 2; i++) { init(i); } }
private void init(final int i) { View inflate = View.inflate(this, R.layout.test, null); final ProgressBar progressBar = (ProgressBar) inflate.findViewById(R.id.id_pro); new Handler().postDelayed(new Runnable() { @Override public void run() { imageProgress.setVisibility(View.GONE); } }, 500); }
從代碼中看,在click()中循環(huán)2次調(diào)用了init()方法并且傳進當前循環(huán)的次數(shù),這個模擬用來模擬instantiateItem()方法調(diào)用,然后再init()方法中獲取ProgressBar對象,并且發(fā)送延遲消息,執(zhí)行進度條隱藏操作,這個是模擬當時的加載圖片成功回調(diào)。沒了,整個過程和代碼都非常簡單。不過有一個地方需要大家注意,這個也是我們這篇文章的主題 Final,和內(nèi)部類,可以看下ProgressBar對象 是局部變量,回調(diào)是匿名內(nèi)部類,想要在匿名內(nèi)部類中使用局部變量,局部變量必須要修飾成為Final的。 這個我想大家都知道,(因為不添加Final會報錯)但是為什么要添加成為Final的我想可能大部分知道,也有小部分人不知道,因為平時就是用,讓我加,我就加,反正不報錯,能正常運行就行(我以前就是這個心里→_→)。那么我就要在這里簡單的說一下為什么加Final,另外 如果你知道就直接往下跳哈,可能看到下面就直接懂了,不知道的就跟我簡單過一下。
Final和內(nèi)部類
為什么匿名內(nèi)部類中使用局部變量,局部變量必須要修飾成為Final ? 這個其實還是因為他們倆生命周期的不一致性,眾所周知,如果我們有一個方法a() 然后,方法中有一個局部變量 i 和一個內(nèi)部類對象P,而且這個P還引用了i,如果我們現(xiàn)在方法執(zhí)行完畢了,i就會隨著方法死亡,但是此時的P對象可就不一定了 (只有沒有人引用該對象了 他才會死亡),這時候就蛋疼了, i 都沒有了,你對象還怎么引用?引用一個不存在的值?別逗了。那么怎么解決這個問題呢?
這時候就要用到我們的Final關鍵字了,為什么是使用Final?而不是其他的關鍵字,這個就要說下他的特性 。大家都知道,什么Final修飾的類不能被繼承啦,final修飾的方法不能被重寫啦,final修飾的變量初始化以后不能被更改啦,這都是他的特性。而Java的開發(fā)者就是用到它的 final修飾的變量初始化以后不能被更改,這條來實現(xiàn)的。他是怎么實現(xiàn)的呢,就是通過final修飾的變量初始化以后不能被更改,值唯一了,保證不變,然后他會賦值一份變量過去給內(nèi)部類使用(如果是基本數(shù)據(jù)類型直接復制,如果是引用類型,復制的是引用地址)。這樣即保證了值得唯一,又保證了內(nèi)部類不會引用一個不存在的值,這時候內(nèi)部類里面已經(jīng)有了一個一毛一樣的變量了,內(nèi)部類就可以訪問了,但是他其實訪問的是i的復制品,并不是源數(shù)據(jù)。
這時你可能會說,你可厲害了,那玩意怎么復制一份的你咋知道,好 那我們看下模擬代碼,(因為我的jd-gui實在是打不開,這里用別人的代碼演示,效果是一樣的)
Java代碼模擬
未編譯前的Java
public static void test(final String s){ //或final String s = "axman"; ABSClass c = new ABSClass(){ public void m(){ int x = s.hashCode(); System.out.println(x); } }; //其它代碼. }
編譯后的Class
public static void test(final String s){ //或final String s = "axman"; class OuterClass$1 extends ABSClass{ private final String s; public OuterClass$1(String s){ this.s = s; } public void m(){ int x = s.hashCode(); System.out.println(x); } }; ABSClass c = new OuterClass$1(s); //其它代碼. }
看到?jīng)]?你以為我們平時在內(nèi)部類中使用局部變量拿過來就用了,是那么簡單的就用了,人家Java在編譯的時候就直接通過內(nèi)部類的構(gòu)造方法把你用到的局部變量傳過去了,這也就是我之前說的,人家內(nèi)部類的是復制過去一份才用的,現(xiàn)在不犟了吧?
好了 上面啰啰嗦嗦了一大堆 ,現(xiàn)在回到正題。那么現(xiàn)在大家都知道了內(nèi)部類用到的局部變量為什么要修飾為Final了,再試著想想,之前遇到的問題,我們執(zhí)行了兩次方法,ProgressBar對象也獲取了兩次,那么按理來說當前的對象應該是最后一個,可事實卻不是,那么我們打印日志看下,還是用上面的代碼。就是加了幾個Log
private void init(int i) { View inflate = View.inflate(this, R.layout.test, null); final ProgressBar progressBar = (ProgressBar) inflate.findViewById(R.id.id_pro); Log.e("TAG", "init i: " + i + " progressBar:" + progressBar); new Handler().postDelayed(new Runnable() { @Override public void run() { Log.e("TAG", "run: progressBar:" + progressBar); } },500); }
日志

看回調(diào),確實是兩個對象的地址,那是為啥呢,其實就是我們上面說的,匿名內(nèi)部類要使用局部變量需要加FInal 在復制一份給自己,然后這個時候內(nèi)部類其實打印的不是上面的ProgressBar對象 而是他內(nèi)部類自己的變量,這樣話就能合理的解釋通上面的現(xiàn)象了,這鬼也算是捉到了吧。
但是我還是不打算停下,我們只是按照他的原理推理出來,也打印出來,但是里面他的內(nèi)部類真的就是分別帶著這個兩者對象么?我這看不見就不行毛病又犯了。所以我打算繼續(xù)斷點跟蹤??!


大家可能一看我去這么多字段,去哪里找那個對象?。?,別著急我們在看下代碼,我們之前使用的是Handler發(fā)送延遲消息來模擬的回調(diào),那么我們就看看有沒有關于Handler的字段,(別問我這么找有的什么依據(jù),我之前就是這么想的,但是也考慮了一點,就是我們發(fā)出去了消息 那么誰來接收呢,這又涉及到了消息,和消息隊列了,這就不說了,但是總感覺是應該由當前Activity的消息隊列來接受消息。) 哎呀 還真找到了,看圖 里面有一個mHandler的字段 我們且先認為他是,打開看看

看到?jīng)] 熟悉東西,mCallback ,mLooper,mMessenger,mQueue, 這不就是Hnadler中消息機制的所有東西么。然后我們在想想,這個對象應該在那個字段里面更合理? 因為我們之前發(fā)送的是延遲消息 而且用的是
postDelayed方法 傳進去的是Runnable對象 不是消息。我們看下源碼。

Runnable對象有傳入到了
getPostMessage()方法 在進去

我去 搞了個Message對象把Runnable賦值給callback了 ,所以還是發(fā)送延遲消息,所以也就是說他把每個對象賦值給Runnable了,又把Runnable賦值給Message的callback了 然后把消息發(fā)了出去,然后在想Handler消息機制一般都是把消息發(fā)送到消息隊列等待輪詢器把他取出來,那我們看看mQueue有沒有,再看之前我們看在一眼Log打出的日志 因為我們是循環(huán)執(zhí)行完畢后打印的,我們就是要看看mQueue中有沒有和打印的地址值是一樣的。


哦 這么多 沒關系我們只看mMessenger 因為消息隊列里面存的都是消息么 ,而我之前也是發(fā)的消息,再打開,

看到?jīng)]有? callback! 我們之前給消息賦值就是賦值的它 此時我的心簡直了,就像是要打開找了好久的寶藏一樣,打開看看,握草!啥都沒有,說好的對象呢?

不過我并不死心,再看看,此時我發(fā)現(xiàn)了個東西 next?下一個? 下一個消息?

為了滿足我的好奇心 打開看看,

咦~~~ 果然tm是 想想也對 人家這里可是消息隊列,就存一個消息算什么消息隊列。如果有好多消息,那剛才的消息里面沒有對象,也是有可能的哈!
不過家看下這個callback為null,說明這個也沒有,那么我們在找next看看那然后在打開callBack,


終于找到了ProgressBar對象,不過 別著急看下地址對不對,a288ee8 是不是之前我們打印的第一個地址 ? 不過另外一個對象呢? 因為我們之前是執(zhí)行兩次 所以會發(fā)兩次消息,那個對象應該在另外一個消息中呆著呢、我們再往下面找。

e3fb901 對不對? 你娘的 終于集齊了! 這個時候如果你在把斷點執(zhí)行完畢你就會看見回調(diào)里面打印出了跟我們找的一毛一樣的引用值。
結(jié)束語
好了 隨著斷點結(jié)束直到兩個對象都找到,大家如果也一直跟我的話,還是有一些收獲的。其實這種小問題,有的時候大家可能稀里糊涂的也解決了,但是你還是不知道他到底為什么這樣。其實在我們實際開發(fā)中不怕碰到Bug,就怕有了Bug不知道動了動那里,好了! 然后你還不知道原因,這就懵逼了! 所以有的時候,如果有時間,還是要多研究研究,就像這次。其實我在公司斷點的時候,是沒有在往下找那么多層的,第一和第二個消息里面就已經(jīng)找到了對象,當我回家在復現(xiàn)的時候,才出現(xiàn)這樣的(RP問題),然后才考慮到這個消息隊列是不是真還有其他消息。然后就找啊找啊找,其實在找的過程中你自己也在思考,這個思考過程你不僅學會了現(xiàn)在的知識,你沒準還會碰到你不知道的一些其他知識。
那么,好啦 這篇文章的解析就到此結(jié)束了 如果你覺得對你有幫助,或者你覺得寫的還不錯,可以點擊喜歡呀,你也可以關注我,當然如果那里寫不對,也歡迎留言指正,改不改再說唄!哈哈 我也會不定時的跟大家分享! 下篇見啊~
