Java小知识-Java中Final和内部类产生的灵异事件

Paste_Image.png

前因

咳咳~ 先说下怎么产生这次的灵异事件,当时的场景是ViewPager中有一个ImageView全屏加载图片,外加一个ProgressBar在图片没有加载完成显示。

界面是酱婶的
Paste_Image.png

代码中大概流程就是当我滑倒第一页的时候就加载图片并且显示进度条,因为是网络加载有延迟,所以就在加载图片成功的回掉里面隐藏进度条。大家可能在想 超级简单是吧? 我当时也是这么想的。

但是想想这里还有些问题,因为我的进度条是写在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); }

日志
Paste_Image.png

看回调,确实是两个对象的地址,那是为啥呢,其实就是我们上面说的,匿名内部类要使用局部变量需要加FInal 在复制一份给自己,然后这个时候内部类其实打印的不是上面的ProgressBar对象 而是他内部类自己的变量,这样话就能合理的解释通上面的现象了,这鬼也算是捉到了吧。

但是我还是不打算停下,我们只是按照他的原理推理出来,也打印出来,但是里面他的内部类真的就是分别带着这个两者对象么?我这看不见就不行毛病又犯了。所以我打算继续断点跟踪!!

Paste_Image.png
Paste_Image.png

大家可能一看我去这么多字段,去哪里找那个对象啊!,别着急我们在看下代码,我们之前使用的是Handler发送延迟消息来模拟的回调,那么我们就看看有没有关于Handler的字段,(别问我这么找有的什么依据,我之前就是这么想的,但是也考虑了一点,就是我们发出去了消息 那么谁来接收呢,这又涉及到了消息,和消息队列了,这就不说了,但是总感觉是应该由当前Activity的消息队列来接受消息。) 哎呀 还真找到了,看图 里面有一个mHandler的字段 我们且先认为他是,打开看看

Paste_Image.png

  看到没 熟悉东西,mCallback ,mLooper,mMessenger,mQueue, 这不就是Hnadler中消息机制的所有东西么。然后我们在想想,这个对象应该在那个字段里面更合理? 因为我们之前发送的是延迟消息 而且用的是postDelayed方法 传进去的是Runnable对象 不是消息。我们看下源码。

Paste_Image.png

Runnable对象有传入到了getPostMessage()方法 在进去

Paste_Image.png

我去 搞了个Message对象把Runnable赋值给callback了 ,所以还是发送延迟消息,所以也就是说他把每个对象赋值给Runnable了,又把Runnable赋值给Message的callback了 然后把消息发了出去,然后在想Handler消息机制一般都是把消息发送到消息队列等待轮询器把他取出来,那我们看看mQueue有没有,再看之前我们看在一眼Log打出的日志 因为我们是循环执行完毕后打印的,我们就是要看看mQueue中有没有和打印的地址值是一样的。

Paste_Image.png
Paste_Image.png

  哦 这么多 没关系我们只看mMessenger 因为消息队列里面存的都是消息么 ,而我之前也是发的消息,再打开,

Paste_Image.png

看到没有? callback! 我们之前给消息赋值就是赋值的它 此时我的心简直了,就像是要打开找了好久的宝藏一样,打开看看,握草!啥都没有,说好的对象呢?

Paste_Image.png

不过我并不死心,再看看,此时我发现了个东西 next?下一个? 下一个消息?


Paste_Image.png

为了满足我的好奇心 打开看看,

Paste_Image.png

咦~~~ 果然tm是 想想也对 人家这里可是消息队列,就存一个消息算什么消息队列。如果有好多消息,那刚才的消息里面没有对象,也是有可能的哈!
不过家看下这个callback为null,说明这个也没有,那么我们在找next看看那然后在打开callBack,


Paste_Image.png
Paste_Image.png

终于找到了ProgressBar对象,不过 别着急看下地址对不对,a288ee8 是不是之前我们打印的第一个地址 ? 不过另外一个对象呢? 因为我们之前是执行两次 所以会发两次消息,那个对象应该在另外一个消息中呆着呢、我们再往下面找。

Paste_Image.png

e3fb901 对不对? 你娘的 终于集齐了! 这个时候如果你在把断点执行完毕你就会看见回调里面打印出了跟我们找的一毛一样的引用值。

结束语

好了 随着断点结束直到两个对象都找到,大家如果也一直跟我的话,还是有一些收获的。其实这种小问题,有的时候大家可能稀里糊涂的也解决了,但是你还是不知道他到底为什么这样。其实在我们实际开发中不怕碰到Bug,就怕有了Bug不知道动了动那里,好了! 然后你还不知道原因,这就懵逼了! 所以有的时候,如果有时间,还是要多研究研究,就像这次。其实我在公司断点的时候,是没有在往下找那么多层的,第一和第二个消息里面就已经找到了对象,当我回家在复现的时候,才出现这样的(RP问题),然后才考虑到这个消息队列是不是真还有其他消息。然后就找啊找啊找,其实在找的过程中你自己也在思考,这个思考过程你不仅学会了现在的知识,你没准还会碰到你不知道的一些其他知识。
   
  那么,好啦 这篇文章的解析就到此结束了 如果你觉得对你有帮助,或者你觉得写的还不错,可以点击喜欢呀,你也可以关注我,当然如果那里写不对,也欢迎留言指正,改不改再说呗!哈哈 我也会不定时的跟大家分享! 下篇见啊~

Paste_Image.png
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,294评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,493评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,790评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,595评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,718评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,906评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,053评论 3 410
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,797评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,250评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,570评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,711评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,388评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,018评论 3 316
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,796评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,023评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,461评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,595评论 2 350

推荐阅读更多精彩内容

  • 1. Java基础部分 基础部分的顺序:基本语法,类相关的语法,内部类的语法,继承相关的语法,异常的语法,线程的语...
    子非鱼_t_阅读 31,598评论 18 399
  • 1.import static是Java 5增加的功能,就是将Import类中的静态方法,可以作为本类的静态方法来...
    XLsn0w阅读 1,217评论 0 2
  • 面向对象主要针对面向过程。 面向过程的基本单元是函数。 什么是对象:EVERYTHING IS OBJECT(万物...
    sinpi阅读 1,046评论 0 4
  • Java 内部类 分四种:成员内部类、局部内部类、静态内部类和匿名内部类。 1、成员内部类: 即作为外部类的一个成...
    ikaroskun阅读 1,226评论 0 13