前因
咳咳~ 先说下怎么产生这次的灵异事件,当时的场景是ViewPager中有一个ImageView全屏加载图片,外加一个ProgressBar在图片没有加载完成显示。
界面是酱婶的
代码中大概流程就是当我滑倒第一页的时候就加载图片并且显示进度条,因为是网络加载有延迟,所以就在加载图片成功的回掉里面隐藏进度条。大家可能在想 超级简单是吧? 我当时也是这么想的。
但是想想这里还有些问题,因为我的进度条是写在Item里面的,也就是说我的每个ViewPager页面都有一个ProgressBar对象需要控制它显示和隐藏。又因为ViewPager是有预加载下一页的特性,当我一看到页面就会加载本和下一页 也就是instantiateItem()
方法会执行两次,那么会导致当前instantiateItem()
方法中的ProgressBar对象不再是第一个页面的对象,而是第二个页面的对象,然后当图片加载成功,回调方法执行隐藏进度条的时候,其实隐藏的是第二个页面的进度条,然而你现在却只是在第一个页面。如果执行代码的话,就会像我上面的说的那样,是不行的ProgressBar
对象是会变的 ,最起码我之前是这么想的,但是写都写了,执行以下试试。执行后发现,我去! 好使竟然,每一个页面都能控制自己的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()
中循环2次调用了init()
方法并且传进当前循环的次数,这个模拟用来模拟instantiateItem()
方法调用,然后再init()
方法中获取ProgressBar
对象,并且发送延迟消息,执行进度条隐藏操作,这个是模拟当时的加载图片成功回调。没了,整个过程和代码都非常简单。不过有一个地方需要大家注意,这个也是我们这篇文章的主题 Final,和内部类,可以看下ProgressBar
对象 是局部变量,回调是匿名内部类,想要在匿名内部类中使用局部变量,局部变量必须要修饰成为Final的。 这个我想大家都知道,(因为不添加Final会报错)但是为什么要添加成为Final的我想可能大部分知道,也有小部分人不知道,因为平时就是用,让我加,我就加,反正不报错,能正常运行就行(我以前就是这个心里→_→)。那么我就要在这里简单的说一下为什么加Final,另外 如果你知道就直接往下跳哈,可能看到下面就直接懂了,不知道的就跟我简单过一下。
Final和内部类
为什么匿名内部类中使用局部变量,局部变量必须要修饰成为Final ? 这个其实还是因为他们俩生命周期的不一致性,众所周知,如果我们有一个方法a()
然后,方法中有一个局部变量 i 和一个内部类对象P,而且这个P还引用了i,如果我们现在方法执行完毕了,i就会随着方法死亡,但是此时的P对象可就不一定了 (只有没有人引用该对象了 他才会死亡),这时候就蛋疼了, i 都没有了,你对象还怎么引用?引用一个不存在的值?别逗了。那么怎么解决这个问题呢?
这时候就要用到我们的Final关键字了,为什么是使用Final?而不是其他的关键字,这个就要说下他的特性 。大家都知道,什么Final修饰的类不能被继承啦,final修饰的方法不能被重写啦,final修饰的变量初始化以后不能被更改啦,这都是他的特性。而Java的开发者就是用到它的 final修饰的变量初始化以后不能被更改,这条来实现的。他是怎么实现的呢,就是通过final修饰的变量初始化以后不能被更改,值唯一了,保证不变,然后他会赋值一份变量过去给内部类使用(如果是基本数据类型直接复制,如果是引用类型,复制的是引用地址)。这样即保证了值得唯一,又保证了内部类不会引用一个不存在的值,这时候内部类里面已经有了一个一毛一样的变量了,内部类就可以访问了,但是他其实访问的是i的复制品,并不是源数据。
这时你可能会说,你可厉害了,那玩意怎么复制一份的你咋知道,好 那我们看下模拟代码,(因为我的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); //其它代码. }
看到没?你以为我们平时在内部类中使用局部变量拿过来就用了,是那么简单的就用了,人家Java在编译的时候就直接通过内部类的构造方法把你用到的局部变量传过去了,这也就是我之前说的,人家内部类的是复制过去一份才用的,现在不犟了吧?
好了 上面啰啰嗦嗦了一大堆 ,现在回到正题。那么现在大家都知道了内部类用到的局部变量为什么要修饰为Final了,再试着想想,之前遇到的问题,我们执行了两次方法,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); }
日志
看回调,确实是两个对象的地址,那是为啥呢,其实就是我们上面说的,匿名内部类要使用局部变量需要加FInal 在复制一份给自己,然后这个时候内部类其实打印的不是上面的ProgressBar
对象 而是他内部类自己的变量,这样话就能合理的解释通上面的现象了,这鬼也算是捉到了吧。
但是我还是不打算停下,我们只是按照他的原理推理出来,也打印出来,但是里面他的内部类真的就是分别带着这个两者对象么?我这看不见就不行毛病又犯了。所以我打算继续断点跟踪!!
大家可能一看我去这么多字段,去哪里找那个对象啊!,别着急我们在看下代码,我们之前使用的是Handler发送延迟消息来模拟的回调,那么我们就看看有没有关于Handler的字段,(别问我这么找有的什么依据,我之前就是这么想的,但是也考虑了一点,就是我们发出去了消息 那么谁来接收呢,这又涉及到了消息,和消息队列了,这就不说了,但是总感觉是应该由当前Activity的消息队列来接受消息。) 哎呀 还真找到了,看图 里面有一个mHandler的字段 我们且先认为他是,打开看看
看到没 熟悉东西,mCallback ,mLooper,mMessenger,mQueue, 这不就是Hnadler中消息机制的所有东西么。然后我们在想想,这个对象应该在那个字段里面更合理? 因为我们之前发送的是延迟消息 而且用的是
postDelayed
方法 传进去的是Runnable对象 不是消息。我们看下源码。
Runnable对象有传入到了
getPostMessage()
方法 在进去
我去 搞了个Message对象把Runnable赋值给callback了 ,所以还是发送延迟消息,所以也就是说他把每个对象赋值给Runnable了,又把Runnable赋值给Message的callback了 然后把消息发了出去,然后在想Handler消息机制一般都是把消息发送到消息队列等待轮询器把他取出来,那我们看看mQueue有没有,再看之前我们看在一眼Log打出的日志 因为我们是循环执行完毕后打印的,我们就是要看看mQueue中有没有和打印的地址值是一样的。
哦 这么多 没关系我们只看mMessenger 因为消息队列里面存的都是消息么 ,而我之前也是发的消息,再打开,
看到没有? callback! 我们之前给消息赋值就是赋值的它 此时我的心简直了,就像是要打开找了好久的宝藏一样,打开看看,握草!啥都没有,说好的对象呢?
不过我并不死心,再看看,此时我发现了个东西 next?下一个? 下一个消息?
为了满足我的好奇心 打开看看,
咦~~~ 果然tm是 想想也对 人家这里可是消息队列,就存一个消息算什么消息队列。如果有好多消息,那刚才的消息里面没有对象,也是有可能的哈!
不过家看下这个callback为null,说明这个也没有,那么我们在找next看看那然后在打开callBack,
终于找到了ProgressBar
对象,不过 别着急看下地址对不对,a288ee8 是不是之前我们打印的第一个地址 ? 不过另外一个对象呢? 因为我们之前是执行两次 所以会发两次消息,那个对象应该在另外一个消息中呆着呢、我们再往下面找。
e3fb901 对不对? 你娘的 终于集齐了! 这个时候如果你在把断点执行完毕你就会看见回调里面打印出了跟我们找的一毛一样的引用值。
结束语
好了 随着断点结束直到两个对象都找到,大家如果也一直跟我的话,还是有一些收获的。其实这种小问题,有的时候大家可能稀里糊涂的也解决了,但是你还是不知道他到底为什么这样。其实在我们实际开发中不怕碰到Bug,就怕有了Bug不知道动了动那里,好了! 然后你还不知道原因,这就懵逼了! 所以有的时候,如果有时间,还是要多研究研究,就像这次。其实我在公司断点的时候,是没有在往下找那么多层的,第一和第二个消息里面就已经找到了对象,当我回家在复现的时候,才出现这样的(RP问题),然后才考虑到这个消息队列是不是真还有其他消息。然后就找啊找啊找,其实在找的过程中你自己也在思考,这个思考过程你不仅学会了现在的知识,你没准还会碰到你不知道的一些其他知识。
那么,好啦 这篇文章的解析就到此结束了 如果你觉得对你有帮助,或者你觉得写的还不错,可以点击喜欢呀,你也可以关注我,当然如果那里写不对,也欢迎留言指正,改不改再说呗!哈哈 我也会不定时的跟大家分享! 下篇见啊~