最近面试,对线程有了更深的理解。
问题:
RuntimeException:Can't create handler inside thread that has not called Looper.prepare()。(不能在没有调用Looper.prepare()的线程内创建Handler)
Looper.prepare()是为了创建Looper、MessageQueue。那为什么在工作线程new Handler,需要Loop对象,为什么UI线程就不需要。对象和线程之间是什么关系,
易混淆点是: 多线程时,认为对象属于某一线程。(大错,特错)
对象
- 本质是 内存中的数据,且具有某种固定格式
- 数据是存在于java虚拟机中,java虚拟机用 堆、方法区来存放数据信息
- 数据分为:类信息的描述(类的名称、方法名、父类名称)、实例数据(字段的值,如Dog类的对象 中的名字 小黑)
- 类信息的描述 存放在虚拟机的 方法区(只是叫这个名字,和java中的方法没啥关系) 中
- 实例数据存放在 堆(heap)中,并保存指向 类信息的指针
- 在虚拟机中 堆、方法区 是线程共享的,也就是线程都可以操作的
各线程是共享内存地址空间,共享一个 堆的、所以共享堆 产生的对象。
打个比方:
会计用算盘(内存)算数,算盘上拨出的数字2(Integer对象,数据)。
当用两个手(线程)操作算盘(内存)拨出2+2。
那右手动的时候,不能说2(数据)属于右手(线程)。
线程:
程序的控制流 (手操作的一组流程,当然比喻不准确),表现为一组方法。而对象是方法操作的元素。
所以每个线程的 方法栈是独立的。这样A、B两个线程才能同时执行不同的方法。
Handler
handler可以从工作线程提交任务,然后UI线程执行。
因为Handler对象线程共享,而它们的操作可以分开,所以并不矛盾。
Looper:
其实框架在加载APP时已调用了Looper.prepareMainLooper();这个操作,在ActivityThread main方法(应用入口)里做的。
那既然Looper已经有了,且对象是线程共有的。为什么还要Looper.prepare()在工作线程new Looper。
这就涉及到了ThreadLocal。Looper有个静态字段
static final ThreadLocal<Looper>sThreadLocal
ThreadLocal:
(算盘右边5串珠子(数据),只允许右手(指定的线程)操作)
ThreadLocal像Map。key是线程本身、value是任意对象。通过get、set方法调用。
调用set时,就会把当前Thread作为Key值。调用get时也是如此。
而Looper.prepare()调用了:
sThreadLocal.set(new Looper(quitAllowed));
因框架启动在UI线程,所以ThreadLocal Key值是UI线程,Value为对应的Looper
Handler有属性Looper,new Handler---》Looper.myLooper()---->sThreadLocal.get();
因为工作线程这个Key值没有对应的Value所以就回报错。
解决:
所以从上面可以看出,只要Handler在new时调用 ThreadLocal能get到Looper就行。所以向Handler传Looper参数就行。
new Thread(){
@Override
public void run() {
super.run();
new Handler(Looper.getMainLooper());
}
}.start();
总结:
对象不存在属于某个进程这种描述,对象是数据,进程是操作流程。之所以出现工作线程需要ThreadLocal,将对象和线程产生了关联。