android的消息机制?
Handler Looper Message
1、简介
- Handler机制是一套Android消息传递机制。在Android开发多线程的应用场景中,将工作线程中需更新UI的操作信息 传递到 UI主线程,从而实现 工作线程对UI的更新处理,最终实现异步消息的处理。
- 在Android开发中,为了UI操作是线程安全的,规定了只允许主线程更新Activity里的UI组件。但在实际开发中,存在多个线程并发操作UI组件的情况,导致UI操作线程不安全。故采用Handler消息传递机制,是工作线程需更新UI时,通过Handler通知主线程,从而在主线程中更新UI操作
2、重要概念
-
1、主线程(UI线程、MainThread)
当应用程序第一次启动时,会同时自动开启1条主线程,用于处理UI相关的事件(如更新、操作等) -
2、子线程(工作线程)
人为手动开启的线程,执行耗时操作(如网络请求、数据加载等) -
3、消息(Message)
线程间通讯的数据单元(即Handler接受 & 处理的消息对象),用于存储需要操作的通信信息 -
4、消息队列(Message Queue)
一种数据结构(先进先出),存储Handler发送过来的消息(Message) -
5、处理者(Handler)
Handler为主线程与子线程的通信媒介,是线程消息的主要处理者。用于添加消息(Message)到消息队列(Message Queue),处理循环器(Looper)分派过来的消息(Message) -
6、循环器(Looper)
消息队列(Message Queue)与处理者(Handler)的通信媒介,用于消息循环,即
(1)消息获取:循环取出消息队列(Message Queue)的消息(Message)
(2)消息分发:将取出的消息(Message)发送给对应的处理者(Handler)
每个线程只能拥有1个Looper,1个Looper可绑定多个线程的Handler,即多个线程可往1个Looper所持有的MessageQueue中发送消息,提供线程间通信的可能
3、工作原理
-
步骤一:异步通信准备
在主线程中创建
(1)循环器 对象(Looper)
(2)消息队列 对象(Message Queue)
(3)Handler对象
Looper、Message Queue均属于主线程,创建Message Queue后,Looper自动进入消息循环。此时,Handler自动绑定了主线程的Looper、Message Queue -
步骤二:消息入队
工作线程通过Handler发送消息(Message)到消息队列(Message Queue)中,该消息内容=工作线程对UI的操作 -
步骤三:消息循环
消息出队:Looper循环取出消息队列(Message Queue)中的消息(Message)
消息分发:Looper将去除的消息(Message)发送给创建该消息的处理者(Handler)
在消息循环过程中,若消息队列为空,则线程阻塞。 -
步骤四:消息处理
处理者Handler接受循环器Looper发送过来的消息(Message)
处理者Handler根据消息(Message)进行UI操作
4、线程Thread、循环器Looper、处理者Handler对应关系
(1)1个线程(Thread)只能绑定1个循环器(Looper),但可以有多个处理者
(2)1个循环器(Looper)可绑定多个处理者(Handler)
(3)1个处理者(Handler)只能绑定1个循环器(Looper)
5、使用方式
-
Handler.sendMessage()
-
Handler.post()
-
Handler.sendMessage与Handler.post比较
工作流程类似,区别在于
1、Handler.post不需外部创建消息对象,而是内部根据传入的Runnable对象封装消息对象
2、回调的消息处理方法是:复写Runnable对象的run()
为什么每个线程最多只能有一个Looper?这是怎么实现的?
Looper对象通过ThreadLocalMap保存,一个线程只有一个ThreadLocalMap,ThreadLocalMap保存键值对,该键值对的键为ThreadLocal对象,Looper里的ThreadLocal对象又通过static和final修饰,只能初始化一次。所以只要线程不变,ThreadLocal对象也不变,键也不变,Map中无法插入第二个键值对,因此保证了一个线程只有一个。
Handler为什么会引发内存泄漏?有哪些解决方式?
Handler导致内存泄漏一般发生在发送延迟消息的时候,当Activity关闭之后,延迟消息还没发出,那么主线程中的MessageQueue就会持有这个消息的引用,而这个消息是持有Handler的引用,而handler作为匿名内部类持有了Activity的引用,所以就有了以下的一条引用链。
主线程 —> threadlocal —> Looper —> MessageQueue —> Message —> Handler —> Activity
其根本原因是因为这条引用链的头头,也就是主线程,是不会被回收的,所以导致Activity无法被回收,出现内存泄漏。
解决方案:
1、改写成静态内部类
2、activity在destroy的时候对handler回收
.安卓中常用的四种引用类型是什么?它们的特点分别是什么?
强引用(Strong Reference)
软引用(Soft Reference)
弱引用(Weak Reference)
虚引用(Phantom Reference)
- 强引用(StrongReference):强引用是最为普遍和使用最多的一种引用方式,如果一个对象具有强引用,当内存不足时,虚拟机宁愿抛出OOM异常结束应用,也不会去回收这个对象。例如:A a = new A() 即a对象具有强引用。
- 弱引用(WeakReference):只具有弱引用的对象在垃圾回收线程扫描其所管辖的内存区域时,不管当前内存是否不足都会直接回收它的内存。弱引用可以和引用队列(ReferenceQueue)结合使用,弱引用对象内存回收前,会将当前引用加入到引用队列中
- 软引用(SoftReference):只具有软引用的对象在垃圾回收线程扫描其所管些的内存区域时,会判断当前内存是否不足,如果当前内存不足则回收软引用对象的内存,如果当前内存足够,则不回收。软引用可以和引用队列(ReferenceQueue)结合使用,软引用对象内存回收前,会将当前引用加入到引用队列中
- 虚引用(PhantomReference):如果一个对象仅持有虚引用,那么它就和没有任何引用一样,在任何时候都可能被垃圾回收器回收。虚引用主要用来跟踪对象被垃圾回收器回收的活动,必须配合引用队列(ReferenceQueue)联合使用,虚引用对象内存回收前,会将当前引用加入到引用队列中。
JVM的GC算法都有哪些?详细介绍一下分代算法中新生代部分。'stop the world’是什么意思?
- 1、标记 -清除算法
“标记-清除”(Mark-Sweep)算法,如它的名字一样,算法分为“标记”和“清除”两个阶段:首先标记出所有需要回收的对象,在标记完成后统一回收掉所有被标记的对象。
内存中的对象构成一棵树,当有效的内存被耗尽的时候,程序就会停止,做两件事,第一:标记,标记从树根可达的对象(途中水红色),第二:清除(清楚不可达的对象)。标记清除的时候有停止程序运行,如果不停止,此时如果存在新产生的对象,这个对象是树根可达的,但是没有被标记(标记已经完成了),会清除掉。
缺点:递归效率低性能低;释放空间不连续容易导致内存碎片;会停止整个程序运行;
- 2、复制算法
“复制”(Copying)的收集算法,它将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这一块的内存用完了,就将还存活着的对象复制到另外一块上面,然后再把已使用过的内存空间一次清理掉。
把内存分成两块区域:空闲区域和活动区域,第一还是标记(标记谁是可达的对象),标记之后把可达的对象复制到空闲区,将空闲区变成活动区,同时把以前活动区对象1,4清除掉,变成空闲区。
速度快但耗费空间,假定活动区域全部是活动对象,这个时候进行交换的时候就相当于多占用了一倍空间,但是没啥用。
- 3、标记-压缩算法
标记过程仍然与“标记-清除”算法一样,但后续步骤不是直接对可回收对象进行清理,而是让所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存
分代收集算法,“分代收集”(Generational Collection)算法,把Java堆分为新生代和老年代,这样就可以根据各个年代的特点采用最适当的收集算法。
JVM垃圾回收分代收集算法:
综合了上述算法优略
1, 分代GC在新生代的算法:采用了GC的复制算法,速度快,因为新生代一般是新对象,都是瞬态的用了可能很快被释放的对象。
2, 分代GC在年老代的算法 标记/整理算法,GC后会执行压缩,整理到一个连续的空间,这样就维护着下一次分配对象的指针,下一次对象分配就可以采用碰撞指针技术,将新对象分配在第一个空闲的区域。
- Java中Stop-The-World机制简称STW,是在执行垃圾收集算法时,Java应用程序的其他所有线程都被挂起(除了垃圾收集帮助器之外)。Java中一种全局暂停现象,全局停顿,所有Java代码停止,native代码可以执行,但不能与JVM交互;这些现象多半是由于gc引起。
android的事件传递机制。如何解决滑动冲突?
首先应该搞清楚两个问题:事件分发机制分发的是什么?怎么进行分发?
分发的是MotionEvent事件了,因而我们讨论的问题就成了当MotionEvent事件生成之后,事件是怎么传递到某一个View控件上面并且得到处理的过程;
android事件产生后的传递过程是从Activity—>Window—>View的,即隧道式传递,而View又分为不包含子 View的View以及包含子View的ViewGroup,事件产生之后首先传递到Activity上面,而Activity接着会传递到 PhoneWindow上,PhoneWindow会传递给RootView,而RootView其实就是DecorView了,接下来便是从 DecorView到View上的分发过程了,具体就可以分成ViewGroup和View的分发两种情况了;
对于ViewGroup而言,当事件分发到当前ViewGroup上面的时候,首先会调用他的dispatchTouchEvent方法,在 dispatchTouchEvent方法里面会调用onInterceptTouchEvent来判断是否要拦截当前事件,如果要拦截的话,就会调用 ViewGroup自己的onTouchEvent方法了,如果onInterceptTouchEvent返回false的话表示不拦截当前事件,那么 事件将会继续往当前ViewGroup的子View上面传递了,如果他的子View是ViewGroup的话,则重复ViewGroup事件分发过程,如 果子View就是View的话,则转到下面的View分发过程;
对于View而言,事件传递过来首先当然也是执行他的dispatchTouchEvent方法了,如果我们为当前View设置了 onTouchListener监听器的话,首先就会执行他的回调方法onTouch了,这个方法的返回值将决定事件是否要继续传递下去了,如果返回 false的话,表示事件没有被消费,还会继续传递下去,如果返回true的话,表示事件已经被消费了,不再需要向下传递了;如果返回false,那么将 会执行当前View的onTouchEvent方法,如果我们为当前View设置了onLongClickListener监听器的话,则首先会执行他的 回调方法onLongClick,和onTouch方法类似,如果该方法返回true表示事件被消费,不会继续向下传递,返回false的话,事件会继续 向下传递,为了分析,我们假定返回false,如果我们设置了onClickListener监听器的话,则会执行他的回调方法onClick,该方法是 没有返回值的,所以也是我们事件分发机制中最后执行的方法了;可以注意到的一点就是只要你的当前View是clickable或者 longclickable的,View的onTouchEvent方法默认都会返回true,也就是说对于事件传递到View上来说,系统默认是由 View来消费事件的,但是ViewGroup就不是这样了;
上面的事件分发过程只是正常情况下的,如果有这样一种情况,比如事件传递到最里层的View之后,调用该View的oonTouchEvent方法返回了 false,那么这时候事件将通过冒泡式的方式向他的父View传递,调用它父View的onTouchEvent方法,如果正好他的父View的 onTouchEvent方法也返回false的话,这个时候事件最终将会传递到Activity的onTouchEvent方法了,也就是最终就只能由 Activity自己来处理了;
事件分发机制需要注意的几点:
(1):如果说除Activity之外的View都没有消费掉DOWN事件的话,那么事件将不再会传递到Activity里面的子View了,将直接由Activity自己调用自己的onTouchEvent方法来处理了;
(2):一旦一个ViewGroup决定拦截事件,那么这个事件序列剩余的部分将不再会由该ViewGroup的子View去处理了,即事件将在此 ViewGroup层停止向下传递,同时随后的事件序列将不再会调用onInterceptTouchEvent方法了;
(3):如果一个View开始处理事件但是没有消费掉DOWN事件,那么这个事件序列随后的事件将不再由该View来处理,通俗点讲就是你自己没能力就别瞎BB,要不以后的事件就都不给你了;
(4):View的onTouchEvent方法是否执行是和他的onTouchListener回调方法onTouch的返回值息息相关 的,onTouch返回false,onTouchEvent方法不执行;onTouch返回false,onTouchEvent方法执行,因为 onTouchEvent里面会执行onClick,所以造成了onClick是否执行和onTouch的返回值有了关系;
如何解决滑动冲突?
在自定义View的过程经常会遇到滑动冲突问题,一般滑动冲突的类型有三种:(1)外部View滑动方向和内部View滑动方向不一致;(2)外部View滑动方向和内部View滑动方向一致;(3)上述两种情况的嵌套;
一般我们解决滑动冲突都是利用的事件分发机制,有两种方式外部拦截法和内部拦截法:
- 外部拦截法:实 现思路是事件首先是通过父容器的拦截处理,如果父容器不需要该事件的话,则不拦截,将事件传递到子View上面,如果父容器决定拦截的话,则在父容器的 onTouchEvent里面直接处理该事件,这种方法符合事件分发机制;具体实现措施是修改父容器的onInterceptTouchEvent方法, 在达到某一条件的时候,让该方法直接返回true就可以把事件拦截下来进而调用自己的onTouchEvent方法来处理了,但是有一点需要注意的是如果 想要让子View能够收到事件,我们需要在onInterceptTouchEvent方法里面判断如果是DOWN事件的话,返回false,这样后续的MOVE以及UP事件才有机会传递到子View上面,如果你直接在onInterceptTouchEvent方法里面DOWN情况下返回了true,那么后续的MOVE以及UP事件将由当前View的onTouchEvent处理了,这样你的拦截将根本没有意义的,拦截只是在满足一定条件才会拦截,并不是所有情况下都拦截;
- 内部拦截法:实 现思路是事件从父容器传递到子View上面,父容器不做任何干预性的措施,所有的事件都会传递到子View上面,如果子元素需要改事件,那么就由子元素消 耗掉了,该事件也就不会回传了,如果子元素不需要该事件,那么他就会回传给父容器来处理了;具体实现措施需要借助于 requestDisallowInterceptTouchEvent方法,该方法用来告诉父容器要不要拦截当前事件,为了配合子View能够调用这个 方法成功,父容器必须默认能够拦截除了DOWN事件以外的事件,为什么要除了DOWN事件以外呢?因为如果一旦父容器拦截了DOWN事件,那么后续事件将 不再会传递到子元素了,内部拦截法也就失去作用了;
- 个人认为外部拦截法是符合正常逻辑的,按照事件隧道式分发过程,如果父容器需要就直接拦截,不需要则传递到子View;内部拦截法相当于人为干预分发这个 过程,我会保证事件先都到子View上面,至于子View需不需要就要看我自己了,如果我不需要就回传给父容器了,需要的话自己就消耗掉了;感觉这两种方 式只是父容器和子View处理事件的优先级不同而已;
HashMap的实现。红黑树的性质?
- 简介
HashMap就是最基础最常用的一种Map,它无序,以散列表(数组+链表/红黑树)的方式进行存储,存储内容是键值对映射。是一种非同步的容器类,故它的线程不安全。
- HashMap继承于AbstractMap类,实现了Map接口。Map是"key-value键值对"接口,AbstractMap实现了"键值对"的通用函数接口。
- HashMap是通过"拉链法"实现的哈希表。它包括几个重要的成员变量:table, size, threshold, loadFactor, modCount。
- table是一个Entry[]数组类型,而Entry实际上就是一个单向链表。哈希表的"key-value键值对"都是存储在Entry数组中的。
- size是HashMap的大小,它是HashMap保存的键值对的数量。
- threshold是HashMap的阈值,用于判断是否需要调整HashMap的容量。threshold的值=“容量*加载因子”,当HashMap中存储数据的数量达到threshold时,就需要将HashMap的容量加倍。
- loadFactor就是加载因子。
- modCount是用来实现fail-fast机制的。
HashMap存储结构
HashMap存储结构由数组和单向链表共同完成的,如图:
从上图可以看出HashMap是Y轴方向是数组,X轴方向就是链表的存储方式。大家都知道数组的存储方式在内存的地址是连续的,大小固定,一旦分配不能被其他引用占用。它的特点是查询快,时间复杂度是O(1),插入和删除的操作比较慢,时间复杂度是O(n),链表的存储方式是非连续的,大小不固定,特点与数组相反,插入和删除快,查询速度慢。