1、Android为什么不能在子线程中更新UI?
原因是UI控件不是线性安全的,所以在多线中的并发访问可能会导致UI控件处于不可预料的状态。不加锁机制的原因一个是加锁会让UI访问的逻辑变得复杂,二是锁机制会降低UI访问的效率,因为锁机制会阻塞某些线程的执行。但是有的时候就要在把一些耗时的操作放在子线程中进行,比如下载。但是下载又会返回结果处理一些UI操作,比如显示下载进度条或者输出下载状态。这个时候就需要把UI操作从子线程切回到主线程去执行。这就要用到Handler机制
2、Hnadler消息机制(以更新UI为例)
(1)首先在主线程中定义Hnadler类,Hnadler类想要执行,就必须要创建Looper,创建Looper的方法是Looper.prepare(),创建Looper的同时就会创建MesssageQueue,MessageQueue叫做消息队列,其实它的内部是一个单链表。但是在主函数中不用自己创建Looper,因为在主线程中有一个方法是prepareMainLooper,这个方法会自动给主线程中创建Looper。(每个线程只能有一个Looper和MessageQueue,但是可以有很多Handler)
(2)然后在子线程中创建要向主线程发送关于更新UI的消息Message实例。Hnadler的post(Runnable)或者send方法会发送这个消息至消息队列。发送的时候就会调用消息队列的enqueueMessage方法把消息插入到消息队列中。然后Looper的loop方法就会无限循环的从消息队列中取出消息送至Handler的dispatchMessage方法中,在取出消息的时候调用消息队列的next方法把消息从队列中取出。
(3)Handler的dispatchMessage方法会检测这个消息是通过哪种方式发送的(post还是send)。如果是post调用handleCallback来处理消息(执行逻辑,更新UI)。要是send就调用handleMessage来处理消息(更新逻辑)
只有发送消息是在子线程中进行的,其他的都是在主线程进行的,所以就这样从子线程切换到主线程。
3、Handler是怎样获取当前线程的Looper的?(ThreadLocal的工作原理)
通过ThreadLocal。
ThreadLocal工作原理
(1)ThreadLocal的作用是他可以在指定的线程中存储数据,存储的数据只有在本线程才可以访问,其他线程都不可以访问。也就是说同一个数据,存放在不同的线程中,在一个线程中对其改变值,其他线程中不会受到影响。所以当某些数据是以线程为作用域并且不同线程具有不同的数据副本的时候,就可以考虑采用ThreadLocal。所以Handler要获取当前线程的Looper就可以使用ThreadLocal。
(2)ThreadLocal的工作原理。
4、MessageQueue工作原理
(1)MessageQueue通过enqueueMessage方法像队列中插入数据
(2)MessageQueue通过next从数列中获取数据
next是一个无线循环的方法,要是消息队列里面没有消息,next方法就会阻塞在那里,要是有消息就会返回这个消息并把这个消息从单链表中删除。
5、Looper的工作原理
(1)调用prepare()方法来创建Looper。但是在主函数中不用自己创建Looper,因为在主线程中有一个方法是prepareMainLooper,这个方法会自动给主线程中创建Looper。
(2)Looper可以调用quit或者quitSafely方法来退出Looper。前者直接退出Looper,后者是把消息队列的消息都处理完再退出。
(3)Lopper的loop是一个死循环,唯一退出循环的方式就是MessageQueue的next方法返回的是一个null。当Looper的quit方法调用的时候,就会调用MessageQueue的quit或者quitSafely方法来通知消息队列退出,此时next就会返回NUll,所以此时loop方法也就跳出循环。
6、Hnadler的工作原理
(1)发送消息是post或者send方法,其实post最终也是通过send发送的。
(2)Handler通过Looper的loop收到消息后会交到Hnadler的dispatchMessage方法进行处理。这个Handler的dispatchMessage方法会检测这个消息是通过哪种方式发送的(post还是send)。如果是post调用handleCallback来处理消息(执行逻辑,更新UI)。要是send就调用handleMessage来处理消息(更新逻辑)
判断方式如下:
首先检查Message的callback是否为null,如果不是空就代表是post发送的,因为callback是一个Runnable对象,也就是post方法传递的Runnable对象。那就调用handlerCallback。
如果为null。那就是send发送的,调用handleMessage方法。
7、主线程的Looper.loop()一直无线循环为什么不会造成ANR
主线程Looper从消息队列读取消息,当读完所有消息时,主线程阻塞。子线程往消息队列发送消息,并且往管道文件写数据,主线程即被唤醒,从管道文件读取数据,主线程被唤醒只是为了读取消息,当消息读取完毕,再次睡眠。因此loop的循环并不会对CPU性能有过多的消耗。
8、Handler为什么会造成内存泄漏以及怎么解决
(1)因为当利用非静态内部类或者匿名类来创建Handler对象的时候。Handler对象会隐式地持有一个外部类对象(通常是一个Activity)的引用(不然你怎么可能通过Handler来操作Activity中的View?)。这就出现一个情况,比如果Activity本来已经没有用了要进行GC了。但由于这时线程尚未执行完,而该线程持有Handler的引用(不然它怎么发消息给Handler?),这个Handler又持有Activity的引用,就导致该Activity无法被回收(即内存泄露)。
(2)解决方法:
A、通过静态内部类来创建Handler的对象,因为静态内部类不会持有外部类的对象。但是不在持有外部类的对象那么怎么操作Activity的UI操作呢。这里加入一个Activity的弱引用
B、在关闭Activity的时候停掉你的后台线程。线程停掉了,就相当于切断了Handler和外部连接的线,Activity自然会在合适的时候被回收。
9、Android中的线程池的作用
线程池中会缓存一定数量的线程。
(1)线程池会重用线程池中的线程,避免线程创建以及销毁带来的开销。
(2)能有效控制线程池最大并发数,避免大量的线程之间因为互相抢占资源而导致的阻塞现象。
(3)能够对线程进行一定的管理。提供定时执行以及指定间隔循环执行等功能。
10、Android中线程池的实现
Android中的线程池来自于Java中的Executor,Executor是一个接口,真正实现线程池的是ThreadPoolExecutor。ThreadPoolExecutor提供一系列的参数来配置线程池,不同的配置会创建不同的线程池。Android中的线程池都是直接或者间接通过配置ThreadPoolExecutor实现的。以下是ThreadPoolExecutor常用的参数(ThreadPoolExecutor构造方法中的参数)
(1)corePoolSize
线程池的核心线程数,默认情况下,他们会一直存活,即使他们处于闲置状态
(2)maximumPoolSize
线程池能容纳的最大线程数,当活动线程数超过这个数量,后续的新任务将会被阻塞
(3)keepAliveTime
非核心线程池如果闲置时间超过这个时间,非核心线程就会被回收。当ThreadPoolExecutor的allowCoreThreadTimeOut的属性设置为true的时候,这个keepAliveTime时间同样也受用6于核心线程
(4)workQueue
线程池中的任务队列,通过线程池的execute方法提交的Runnable对象会存储在这个参数中
(5)threadFactory
线程工厂,为线程池创建新线程的功能。ThreadFactory是一个接口,他只有一个方法:
Thread newThread(Runnable r)
线程池的执行任务的时候遵守的规则:
(1)当线程池中线程数量没有达到核心线程的数量,那么直接开启一个核心线程来执行任务
(2)当线程池中线程数量已经达到了或者超过了核心线程的数量,那么任务就会被插入到任务队列中排队然后等带被执行
(3)如果在(2)中任务不能插入到队列中的话,那么就说明任务队列满了,这个时候如果线程数量没有达到线程池中规定的最大值,那么就开启一个非核心线程来执行任务。
(4)如果(3)中线程池中线程数量已经达到最大值,就拒绝执行任务。抛出异常。
11、Android中的4类线程池
Android中的线程池都是直接或者间接通过配置ThreadPoolExecutor得到的
(1)FixedThreadPool(固定的)
里面全是核心线程且数量固定,不会被回收,没有超时机制,而且由于任务队列的大小没有限制,当所有线程都在活动的时候,新任务就只能等待。他可以更加快速的响应外界的请求。
(2)CachedThreadPool(缓存的)
CachedThreadPool只有非核心线程,最大线程数非常大,所有线程都活动时,会为新任务创建新线程,否则利用空闲线程(60s空闲时间,过了就会被回收,所以线程池中有0个线程的可能就是都是空闲而闲置超过60s)处理任务。它的任务队列其实是一个空的队列,其实这个很好理解,因为它的线程池超级大,只要来的任务都会被空闲的线程所执行或者新建线程去执行。
所以这个线程池的任何任务都会被立刻响应,适用于大量的耗时较少的任务。当整个线程池都处于空闲状态的时候,线程池中的线程会因为超时被停止,这个时候线程池就是空的,几乎不占用任何资源。
(3)ScheduledThreadPool(计划的)
核心线程数量是固定的,但是非核心线程数量是没有限制的。并且当非核心的线程闲置的时候会被立刻回收。所以适合做定时任务或者具有固定周期的任务。
(4)SingleThreadExecutor
SingleThreadPool只有一个核心线程,确保所有任务都在同一线程中按顺序完成。因此不需要处理线程同步的问题。
12、AsyncTask的作用与执行。
AsyncTask可以在线程池中执行后台任务,然后再把执行的进度以及最终的结果传递给主线程并且在主线程中更新UI。从事先上来说,AsynvTask封装了线程池和Handler。利用AsyncTask可以更方比那的执行后台任务并且在主线程访问UI。(但是AsyncTask不适合特别耗时的后台任务,特别耗时的后台任务还是建议使用线程池,就是真正的通过那三种方法来创建子线程来完成)
AsyncTask的执行过程:
因为AsyncTask是一个抽象类,所以想要使用它,必须创建一个类来继承它。
class DownloadTask extends AsyncTask<Params,Progress,Result>{
}
这三个泛型参数
(1)第一个是在执行AsyncTask时候要传入的参数,用于后台使用,传给doInBackgound使用。
(2)第二个参数是如果需要在页面上展示下当前的执行进度,则用这个参数的类型来作为进度单位。这里是Integer,表示用整形来作为进度显示单位。doInBackground中publishProgress方法传给onProgressUpdate的。
(3)第三个参数是如果执行完毕后需要对结果进行返回,则使用这个泛型的类型作为返回时类型,doInBackground返回的结果传给onPostExecute的。
下面就以下载的例子来说明AsyncTask中的四个函数和三个参数。
(1)onPreExecute()
主线程中执行,在异步任务执行之前可以做一些初始化操作和准备工作。
(2)doInBackground(Params…params)
这里的操作都是在子线程中进行的,所以不可以进行UI操作,要是想进行UI操作,就在这个方法里调用pulishProgress方法,这个方法就是进行子线程切换到主线程的。
AsyncTask中的第一个参数就是要传进doInBackground()这里来的,在下载任务里,这个参数就是下载地址。那么在doInBackground()中调用pulishProgress方法,pulishProgress方法就会携带AsyncTask中的第二个参数传进到onProgressUpdate这里来。下载任务里第二个参数是一个整型,意味着在主线程中显示下载进度用整型表示。同样doInBackground的返回结果就是第三个参数,第三个参数传递到onPostExecute中,在下载任务中第三个参数也是一个整型,意味着doInBackground返回结果用整型表示,通过这个整型onPostExecute可以判断下载任务的执行状态(成功?失败?取消?)然后在主线程UI中显示出来。
(由此可见,AsyncTask的三个参数都是跟doInBackground有关的)
(3)onProgressUpdate(Progress…values)
在主线程中执行,一旦doInBackground中调用了pulishProgress方法,onProgressUpdate()方法就会被执行,然后在这里进行UI操作
(4)onPostExecute()
在主线程中执行。执行任务收尾工作。
总结来说
onPreExecute()任务开启前初始化工作,
doInBackground()子线程中的耗时工作,如果需要UI,则调用pulishProgress方法切换到主线程。
onProgressUpdate()UI操作。
onPostExecute()任务完成后的收尾工作
只有doInBackground()是在子线程中执行的,其他的都是在主线中执行的。注意一点就是:onProgressUpdate()和onPostExecute()虽然是执行UI操作的,但是不一定UI操作的逻辑就在这两个方法里直接写。比如显示下载进度和显示下载状态这两个函数不一定写在onProgressUpdate()和onPostExecute()中,也可以写在活动或者服务中(真正的UI线程中,因为毕竟 DownloadTask或者说AsyncTask只是一个类)然后onProgressUpdate()和onPostExecute()这两个方法中再去调用UI更新的方法。
开启这个任务
new DownloadTak().execute()
12、AsyncTask的工作原理
13、View的滑动
实现View滑动效果的三种方式
(1)使用scrollTo/scrollBy
通过参数对View中的内容进行移动,只能移动View中的内容,不能移动View控件本身
(2)使用动画
通过改变动画有两种方式,分为传统的View动画和属性动画
这两种动画都是通过改变View的translationX和translationY属性来实现滑动,translationX和translationY是View左上角相对于父容器的偏移量
属性动画更好一些,没有什么问题,但是对于传统的View动画有一个问题就是:
那就是他只能对View的影响进行操作,也就是说当把View移动一段距离,在新位置点击View是没有响应的,反而在老位置是有响应的。也就相当于滑动后的View像原始View的一个分身。遇到这种情况的解决思路是:
就是在新位置上创建一个和老的View一模一样的控件并且点击事件也一样,这样当滑动完事后,就把目标View隐藏,让事先创建的和老的View一模一样的View显示出来。这样通过间接的方式解决问题。
(3)改变参数布局
第一种方案就是改变一个控件在布局中的偏移量,从而达到滑动View的效果
第二种方案就是在控件的旁边放一个View,默认为0,想移动控件,就让这View变大,从而达到滑动View的效果。
对比:
scrollTo/scrollBy:适合对View的内容进行滑动
使用动画:适合无交互的滑动
改变布局参数:适合有交互的滑动
14、View的弹性滑动
所谓弹性滑动就是不让滑动一下子完成,而是在一定时间内循序渐进的完成,符合我们现在的实际操作。无论通过怎样的方式实现弹性滑动,思想都是将一次大的滑动分成若干次小的滑动并在一定的时间段内完成。
(1)使用Scroller
Scroller实现弹性滑动的工作原理:
Scroller本身不可以让View进行移动,他需要配合View的computeScroller来实现弹性滑动。具体实现就是Scroller会不断地让View进行重绘,而每次重绘都是需要时间间隔的,在这个间隔里面Scroller就会计算出View的滑动位置,通过这个滑动位置,scrollerTo就会让View滑动一小段距离。如此不断的重绘然后滑动就会让一次大的滑动分成多次小的滑动并在一定时间内完成从而实现弹性滑动。
(2)使用动画
就是在动画每一帧完成前获取动画完成的比例,然后通过这个比例来计算出View所要滑动的距离
通过百分比配合scrollerTo来完成弹性滑动。
(3)使用延迟
通过发送一系列的延迟消息从而达到一种渐进式的效果。比如使用Handler和View的postDelaed的方法。或者使用Thread的sleep方法,都是通过发送延迟的消息,在这个延迟的消息里让View滑动,然后不断的发送延迟消息,从而达到弹性滑动的效果。
15、View的事件分发机制
(1)点击事件(MotionEvent)
ACTION_DOWN:手指刚按下屏幕
ACTION_MOVE:手指在屏幕上滑动
ACTION_UP:手指离开屏幕的瞬间
(2)所谓View的事件分发机制就是当产生点击事件的时候,找到可以处理这个事件的控件就是View的事件分发机制。
(3)事件分发机制要用到三个函数
dispatchTouchEvent() 当事件传递到当前的View,当前View就会调用这个,表示对事件进行分发
onInterceptTouchEvent() 在dispatchTouchEvent() 中调用,用来判断是否拦截某个事件。
onTouchEvent() 在dispatchTouchEvent() 中调用,用来判断是否处理这个事件
(4)事件分发机制开启前的流程
点击事件到来时候都是先到Activity中的,Activity调用dispatchTouchEvent() 方法进行事件分发,内部实现就是调用Activity内部的Window将事件传递给decor view(一个容器),然后window中的PhoneWindow方法将事件传递给DecorView,最终DecorView将事件传递给顶层的View,顶层的View一般就是ViewGroup。ViewGroup中对于事件的分发就是事件的分发机制
(5)事件的分发机制
首先会调用ViewGroup的dispatchTouchEvent() ,如果dispatchTouchEvent() 中的onInterceptTouchEvent() 返回true就是拦截了这个事件,那么ViewGroup中的onTouchEvent就会被调用,否则的话就是没有拦截这个事件,那么就交给子View处理。
交给子View处理的时候首先会遍历这个子View,看看谁能接受这个事件,所谓能接受这个事件就是说当前的看看子View是否是播发动画或者点击事件的坐标在子View的区域里。如果找到了可以接受事件的子View,就调用这个子View的dispatchTouchEvent() 。如果这个子View拦截了但是onTouchEvent返回了false。这时候分两种情况,第一种就是子View没有消耗ACTION_DOWN,这个时候事件会返回到上一级,且当前子View短期内不会再接到事件了。第二种情况就是消耗了ACTION_DOWN,但是其他的没有消耗,那么这个点击事件会消失,此子View还可以接受别的事件,最终这个事件会回到Activity中。
(6)事件分发事件需要注意的地方
所有要处理事件的View。
onTouch>onTouchEvent>onClick
也就是说如果设置了onTouchListener,且其中的onTouch防范返回了true,机会执行onTouch不会执行onTouchEvent,否则才会执行onTouchEvent,当在onTouchEvent中设置了onClickListener会调用onClick。
(7)只要View的CLICKABLE或者LONG_CLICKABLE有一个是true,那么它的onTouchEvent就会返回true。View中的LONG_CLICKABLE都默认是false,至于CLICKABLE看具体控件,Button就是true。TextView即使false。
16、View的滑动冲突
所谓滑动冲突就是在滑动(Move)过程到底是哪个View可以拦截响应,其实这也是个事件分发的问题。
一般滑动冲突分为三种情况
里外不一致(里面是左右滑动,外面是上下滑动)比如ViewPage+ListView
里外一致
前两种嵌套
解决滑动冲突的两种办法:
(1)外部拦截:简单建议使用
就是滑动是父容器需要的就进行拦截,如果不是父容器需要的就传给子容器
这种方法重写父容器中的onInceptTouchEvent方法,不同的冲突情况只要进行不同的判断父容器是否需要拦截就可以了
(2)内部拦截:复杂
就是都交给子容器处理,子容器需要就拦截,不需要就返回给父容器。重新子容器的dispatchTouchEvent()方法,也是不同的冲突就进行不同的判断父容器是否需要拦截就可以了,如果不需要,那么子View处理,如果需要则交给父容器处理。这个时候还需要设置让父容器接受除了ACTION-DOWN之外的事件,为了是当子View返回给父容器的时候,父容器可以接受这些事件,为什么不让父容器接受ACTION-DOWN呢?因为一旦一个控件接受了ACTION-DOWN,那么就要接受其他的了,就没法传给下一层了。
针对上述的三种冲突类型给出不同的判断父容器是否需要拦截
第一种:如果外部(父)是左右,内部是上下,就判断坐标水平和竖直的差哪个大?如果水平大就是父拦截,否则就是子拦截。
第二种:都是一个方向,都是有业务需求来进行选择判断
第三种:分开处理前两种