参考资料:https://qwerhuan.gitee.io/2020/10/08/android/android-quan-mian-jie-xi-zhi-handler-ji-zhi/
https://zhuanlan.zhihu.com/p/427148330(Handler面试题)
可能有部分图片加载不出,参考我的有道云笔记:
https://note.youdao.com/old-web/#/file/F04BF236EEA948698DEB7365A553D5A5/markdown/WEBfe1142377ad79cd8a2078b9b1c2d2318/
-
Handle消息机制(非常重要),LocalBroadcastManager?
-
什么是Handler?
Handler机制是Android中基于单线消息队列模式的一套线程消息机制。本质是消息机制,负责消息的分发以及处理。
-
Handle原理
Android 中主线程是不能进行耗时操作的,子线程是不能进行更新 UI 的。所以就有了 handler, 它的作用就是实现线程之间的通信。
Handler 整个流程中,主要有四个对象,handler,Message,MessageQueue,Looper。当应用创建的时候,就会在主线程中创建 handler 对象, 我们通过要传送的消息保存到 Message 中,handler 通过调用 sendMessage() 将 Message 发送到 MessageQueue 中,Looper 对象就会不断的调用 loop() 不断的从 MessageQueue 中取出 Message 交给 handler 进行处理。从而实现线程之间的通信。注意:
每个线程只能拥有1个Looper但是可以有多个handler,1个Looper可绑定多个线程的handler,一个handler只能绑定一个looper。
补充:
ThreadLocal 提供了 get/set 方法分别用来获取和保存变量。比如在主线程通过 prepare() 方法来创建 Looper 对象,并使用 sThreadLoacal.set(new Looper(quitAllowed)) 来保存主线程的 Looper 对象,那么在主线程调用 myLooper()(实际调用了 sThreadLocal.get() 方法) 就是通过 ThreadLocal 来获取主线程的 Looper 对象。如果在子线程调用这些方法就是通过对 ThreadLocal 保存和获取属于子线程的 Looper 对象。
内存泄漏:
我们会发现Entry中,ThreadLocal是一个弱引用,而value则是强引用。如果外部没有对ThreadLocal的任何引用,那么ThreadLocal就会被回收,此时其对应的value也就变得没有意义了,但是却无法被回收,这就造成了内存泄露。怎么解决?在ThreadLocal回收的时候记得调用其remove方法把entry移除,防止内存泄露。
Message
Message的作用就是承载消息,他的内部有很多的属性用于给用户赋值。同时Message本身也是一个链表结构,无论是在MessageQueue还是在Message内部的回收机制,都是使用这个结构来形成链表。同时官方建议不要直接初始化Message,而是通过Message.obtain()方法来获取一个Message循环利用。一般来说我们不需要去调用recycle进行回收,在Looper中会自动把Message进行回收
MessageQueue
每个线程都有且只有一个MessageQueue,他是一个用于承载消息的队列,内部使用链表作为数据结构,所以待处理的消息都会在这里排队。
主要包含两个操作:插入和读取(其中,读取操作本身伴随删除操作),对应的方法分别为 enqueueMessage() 和next(),enqueueMessage()的作用就是往消息队列中插入一条消息,虽然叫做消息队列,其实是通过单链表的数据结构来维护消息列表的,因为它在删除和插入上有比较多的优势。enqueueMessage()的实现来看,主要操作就是单链表的插入操作;next(),是一个无限循环的方法,如果消息队列中没有消息,那么next会一直阻塞在这里,当有新消息到来时,next()会返回这条消息并从单链表中移除。
Message还涉及到一个关键概念:线程休眠。当MessageQueue中没有消息或者都在等待中,则会将线程休眠,让出cpu资源,提高cpu的利用效率。进入休眠后,如果需要继续执行代码则需要将线程唤醒。当方法暂时无法直接返回需要等待的时候,则可以将线程阻塞,即休眠,等待被唤醒继续执行逻辑。
Looper
Looper在Android的消息响应机制中扮演消息循环的角色,具体来说就是不停的从 MessageQueue 中查看是否有新消息,如果有就立即处理,否则一直阻塞在那里。Looper最重要的方法是 loop(),只有调用了loop后,消息循环才会真正的起作用。该方法的工作过程是,loop方法是一个死循环,唯一跳出的方式是MessageQueue的next方法返回了null(退出Looper时)。loop()方法会调用MessageQueue的next()方法来获取新消息,而next操作是一个阻塞操作,没有消息时,next会一直阻塞在那里,如果next返回新值,Looper就会处理这条消息:即调用msg.target.dispathMessage(msg),调用message的目标handler的dispatchMessage方法来处理Message。
几个关键方法:
-
prepare(): 初始化Looper
Looper.class static final ThreadLocal
sThreadLocal = new ThreadLocal (); public static void prepare() { prepare(true); } // 最终调用到了这个方法 private static void prepare(boolean quitAllowed) { if (sThreadLocal.get() != null) { throw new RuntimeException("Only one Looper may be created per thread"); } sThreadLocal.set(new Looper(quitAllowed)); } 每个线程使用Handler之前,都必须调用Looper.prepare()方法来初始化当前线程的Looper。参数quitAllowed表示该Looper是否可以退出。主线程的Looper是不能退出的,不然程序就直接终止了。我们在主线程使用Handler的时候是不用初始化Looper的,为什么?因为Activiy在启动的时候就已经帮我们初始化主线程Looper了,所以在主线程我们可以直接调用Looper.myLooper()获取当前线程的Looper。
Looper的构造方法:
private Looper(boolean quitAllowed) { mQueue = new MessageQueue(quitAllowed); mThread = Thread.currentThread(); }
-
myLooper() : 获取当前线程的Looper对象
public static @Nullable Looper myLooper() { return sThreadLocal.get(); }
-
loop()循环获取消息
当Looper初始化完成之后,他是不会自己启动的,需要我们自己去启动Looper,调用Looper的loop()方法
public static void loop() { // 获取当前线程的Looper final Looper me = myLooper(); if (me == null) { throw new RuntimeException("No Looper; Looper.prepare() wasn't called on this thread."); } final MessageQueue queue = me.mQueue; ... for (;;) { // 获取消息队列中的消息 Message msg = queue.next(); // might block if (msg == null) { // 返回null说明MessageQueue退出了 return; } ... try { // 调用Message对应的Handler处理消息 msg.target.dispatchMessage(msg); if (observer != null) { observer.messageDispatched(token, msg); } dispatchEnd = needEndTime ? SystemClock.uptimeMillis() : 0; } ... // 回收Message msg.recycleUnchecked(); } }
-
quit/quitSafely : 退出Looper
quit是直接将Looper退出,quitSafely是将MessageQueue中的不需要等待的消息处理完成之后再退出,看一下代码:
public void quit() { mQueue.quit(false); } // 最终都是调用到了这个方法 void quit(boolean safe) { // 如果不能退出则抛出异常。这个值在初始化Looper的时候被赋值 if (!mQuitAllowed) { throw new IllegalStateException("Main thread not allowed to quit."); } synchronized (this) { // 退出一次之后就无法再次运行了 if (mQuitting) { return; } mQuitting = true; // 执行不同的方法 if (safe) { removeAllFutureMessagesLocked(); } else { removeAllMessagesLocked(); } // 唤醒MessageQueue nativeWake(mPtr); } }
最后都调用了quitSafely方法。这个方法先判断是否能退出,然后再执行退出逻辑。
Hander
Handle的工作主要包括消息的发送和接收过程。发送消息主要通过post和send系列来实现,post最终也是send系列来完成的。Handler是作为整个消息机制的消息发起者与处理者,消息在不同的线程通过Handler发送到目标线程的MessageQueue中,然后目标线程的Looper再调用Handler的dispatchMessage方法来处理消息。
处理消息代码如下:
public void dispatchMessage(@NonNull Message msg) { if (msg.callback != null) { handleCallback(msg); } else { if (mCallback != null) { if (mCallback.handleMessage(msg)) { return; } } handleMessage(msg); } } private static void handleCallback(Message message) { message.callback.run(); }
首先判断Message是否有callBack,有的话就直接执行callBack的逻辑,这个callBack就是我们调用handler的post系列方法传进去的Runnable对象。否则判断Handler是否有callBack,有的话执行他的方法,如果返回true则结束,如果返回false则直接Handler本身的handleMessage方法。
-
创建handler(2种方式)
public class MainActivity extends AppComposeActivity{ ...; // 第一种方法:使用callBack创建handler public void onCreate(Bundle savedInstanceState){ super.onCreate(savedInstanceState); Handler handler = Handler(Looper.myLooper(),new CallBack(){ public Boolean handleMessage(Message msg) { TODO("Not yet implemented") } }); } // 第二种方法:继承Handler并重写handlerMessage方法 static MyHandler extends Hanlder{ public MyHandler(Looper looper){ super(looper); } @Override public void handleMessage(Message msg){ super.handleMessage(msg); // TODO(重写这个方法) } } }
注意:
注意第二种方法,要使用静态内部类,不然可能会造成内存泄露。原因是非静态内部类会持有外部类的引用,而Handler发出的Message会持有Handler的引用。如果这个Message是个延迟的消息,此时activity被退出了,但Message依然在“流水线”上,Message->handler->activity(可达性分析法),那么activity就无法被回收,导致内存泄露。
两种Handler的写法各有千秋,继承法可以写比较复杂的逻辑,callback法适合比较简单的逻辑,看具体的业务来选择。
Handler面试题:
-
为什么在主线程可以直接使用 Handler?
因为主线程已经创建了 Looper 对象并开启了消息循环。prepareMainLooper() 方法主要是使用 prepare(false) 创建当前线程的 Looper 对象,再使用 myLooper() 方法来获取当前线程的 Looper 对象。
public static void main(String[] args) { ... // 初始化主线程Looper Looper.prepareMainLooper(); ... // 新建一个ActivityThread对象 ActivityThread thread = new ActivityThread(); thread.attach(false, startSeq); // 获取ActivityThread的Handler,也是他的内部类H if (sMainThreadHandler == null) { sMainThreadHandler = thread.getHandler(); } ... Looper.loop(); // 如果loop方法结束则抛出异常,程序结束 throw new RuntimeException("Main thread loop unexpectedly exited"); }
-
为什么主线程的Looper是一个死循环,但是却不会ANR?
因为当Looper处理完所有消息的时候会进入阻塞状态,当有新的Message进来的时候会打破阻塞继续执行。
这其实没理解好ANR这个概念。ANR,全名Application Not Responding。当我发送一个绘制UI的消息到主线程Handler之后,经过一定的时间没有被执行,则抛出ANR异常。Looper的死循环,是循环执行各种事务,包括UI绘制事务。Looper死循环说明线程没有死亡,如果Looper停止循环,线程则结束退出了。Looper的死循环本身就是保证UI绘制任务可以被执行的原因之一。同时UI绘制任务有同步屏障,可以更加快速地保证绘制更快执行。
-
Handler如何保证MessageQueue并发访问安全?
循环加锁,配合阻塞唤醒机制。
我们可以发现MessageQueue其实是“生产者-消费者”模型,Handler不断地放入消息,Looper不断地取出,这就涉及到死锁问题。如果Looper拿到锁,但是队列中没有消息,就会一直等待,而Handler需要把消息放进去,锁却被Looper拿着无法入队,这就造成了死锁。Handler机制的解决方法是循环加锁。在MessageQueue的next方法中:
Message next() { ... for (;;) { ... nativePollOnce(ptr, nextPollTimeoutMillis); synchronized (this) { ... } } }
我们可以看到他的等待是在锁外的,当队列中没有消息的时候,他会先释放锁,再进行等待,直到被唤醒。这样就不会造成死锁问题了。
那在入队的时候会不会因为队列已经满了然后一边在等待消息处理一边拿着锁呢?这一点不同的是MessageQueue的消息没有上限,或者说他的上限就是JVM给程序分配的内存,如果超出内存会抛出异常,但一般情况下是不会的。
-
Looper退出后是否可以重新运行?
不可以。
线程的存活是靠Looper调用的next方法进行阻塞实现的。如果Looper退出后,那么线程会马上结束,也不会再有第二次运行的机会了。即使线程还没结束再一次调用loop(),Looper内部有一个mQuitting变量,当他被赋值为false之后就无法再被赋值为true。所以就无法再重新运行了。
-
Handler是如何切换线程的?
使用不同线程的Looper处理消息。
每个Looper都运行在对应的线程,所以不同的Looper调用的dispatchMessage方法就运行在其所在的线程了。(调用msg.target.dispathMessage(msg),调用message的目标handler的dispatchMessage方法来处理Message。)
-
Handler的阻塞唤醒机制是怎么回事?
Handler的阻塞唤醒机制是基于Linux的阻塞唤醒机制。
这个机制也是类似于handler机制的模式。在本地创建一个文件描述符,然后需要等待的一方则监听这个文件描述符,唤醒的一方只需要修改这个文件,那么等待的一方就会收到文件从而打破唤醒。和Looper监听MessageQueue,Handler添加message是比较类似的。
-
什么是IdleHandler?
当MessageQueue为空或者目前没有需要执行的Message时会回调的接口对象。
IdleHandler看起来好像是个Handler,但他其实只是一个有单方法的接口,也称为函数型接口:
public static interface IdleHandler { boolean queueIdle(); }
在MessageQueue中有一个List存储了IdleHandler对象,当MessageQueue没有需要被执行的MessageQueue时就会遍历回调所有的IdleHandler。所以IdleHandler主要用于在消息队列空闲的时候处理一些轻量级的工作。
-
Looper 对象是如何绑定 MessageQueue 的?或者说 Looper 对象创建 MessageQueue 过程?
//该构造方法在 prepare() 中 sThreadLocal.set(new Looper(quitAllowed)); private Looper(boolean quitAllowed) { mQueue = new MessageQueue(quitAllowed); mThread = Thread.currentThread(); }
-
Message 是如何绑定 Handler 的?
Handler 执行发送消息的过程中将自己绑定给了 Message 的 target,这样两者之间就产生了联系
private boolean enqueueMessage(@NonNull MessageQueue queue, @NonNull Message msg, long uptimeMillis) { msg.target = this; msg.workSourceUid = ThreadLocalWorkSource.getUid(); // 赋值 if (mAsynchronous) { msg.setAsynchronous(true); } return queue.enqueueMessage(msg, uptimeMillis); }
-
Handler 如何绑定 MessageQueue?
Handler 绑定的是 Looper 的 MessageQueue 对象,Looper 的 MessageQueue 对象是在 Looper 创建时就 new 的。
public Handler(@NonNull Looper looper, @Nullable Callback callback, boolean async) { mLooper = looper; mQueue = looper.mQueue; mCallback = callback; // 这里赋值 mAsynchronous = async; }
-
关于 handler,在任何地方 new handler 都是什么线程下?
- 不传递 Looper 创建 Handler:Handler handler = new Handler();通过 Looper.myLooper() 来获取 Looper 对象,也就是说对于不传递 Looper 对象的情况下,在哪个线程创建 Handler 默认获取的就是该线程的 Looper 对象,那么 Handler 的一系列操作都是在该线程进行的。
- 对于传递 Looper 对象创建 Handler 的情况下,传递的 Looper 是哪个线程的,Handler 绑定的就是该线程。
-
为什么每个线程对应一个looper?
每个线程里边有一个map,这个map里边有entry键值对里边的key是Threadlocal,value是looper(一个键对应唯一一个值)这里的Threadlocal是static final的所以key只有一个,那value(looper)怎么保证是惟一的呢?原来looper的prepare方法会判断Threadlocal里是不是对应了looper了,如果已经对应就不会再对应了,而是报错。map里只有一个。就这样保证一个线程只有一个looper。
-
Handler内存泄漏
原因是非静态内部类会持有外部类的引用,而Handler发出的Message会持有Handler的引用。如果这个Message是个延迟的消息,此时activity被退出了,但Message依然在“流水线”上,Message->handler->activity,那么activity就无法被回收,导致内存泄露。
message会有一个target里边存放handler,emessage是由messagequeue来处理的,会在里边延时,要是message还在messagequeue没被消费时,message持有Handler的引用,而handler一直持有activity,使他无法回收,导致内存泄漏。
-
关于massage的了解
massage其实当从massagequeue里被消费后,不会直接致空,而是把里边的变量致空,他其实维护了一个池子。当massage被消费后把他里边变量致空后重新放到池子里。当再次使用时候从池子里取。(调用obtain方法)这其实就是享元设计模式。避免了多次new对象的过程。这样可以避免内存抖动。
-
-
View的事件分发机制,滑动冲突;ACTION_DOWN,ACTION_MOVE,ACTION_UP,ACTION_CANCEL
-
view的分发机制
点击事件产生后,这个事件被分装成一个类:MotionEvent。系统首先会将事件传递给当前的 Activity,Activity会调用它的 dispatchTouchEvent 方法,将事件交给 PhoneWindow,通过 PhoneWindow 传递给 DecorView,然后再传递给根 ViewGroup,进入 ViewGroup 的 dispatchTouchEvent 方法,执行 onInterceptTouchEvent() ,如果ViewGroup 的 onInterceptTouchEvent() 返回true,表示它要拦截这个事件,false表示不拦截,再不拦截的情况下,此时会遍历 ViewGroup 的子元素,进入子 View 的 dispatchToucnEvent 方法,如果子 view 设置了 onTouchListener,不为null,就执行 onTouch 方法,并根据 onTouch 的返回值为 true 还是 false 来决定是否执行 onTouchEvent 方法,如果是 true,则表示事件被消费了,不会执行onTouchEvent(),如果是 false 则继续执行 onTouchEvent,可见onTouchListener优先级>onTouch>onTouchEvent。在源码中的话可以看到,只要View的CLICKABLE和LONG_CLICKABLE有一个为true,该方法就会返回true消耗这件事,在 onTouchEvent 的 ACTION_UP 事件发生时会触发 performClick(),如果View设置了 onClickListener ,就会调用 performClick() 中的 onClick()。
注意:
ViewGroup默认不拦截任何事件
View没有onInterceptTouchEvent方法,一旦有点击事件交给他,onTouchEvent()一定会被调用
-
View的onTouchEvent()默认都会消费事件(返回true),除非长短点击都为False
// 发生ACTION_DOWN事件或者已经发生过ACTION_DOWN,并且将mFirstTouchTarget赋值,才进入此区域,主要功能是拦截器 final boolean intercepted; if (actionMasked == MotionEvent.ACTION_DOWN|| mFirstTouchTarget != null) { //disallowIntercept:是否禁用事件拦截的功能(默认是false),即不禁用 //可以在子View通过调用requestDisallowInterceptTouchEvent方法对这个值进行修改,不让该View拦截事件 final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0; //默认情况下会进入该方法 if (!disallowIntercept) { //调用拦截方法 intercepted = onInterceptTouchEvent(ev); ev.setAction(action); } else { intercepted = false; } } else { // 当没有触摸targets,且不是down事件时,开始持续拦截触摸。 intercepted = true; }
这一段的内容主要是为判断是否拦截。如果当前事件的MotionEvent.ACTION_DOWN,则进入判断,调用ViewGroup.onInterceptTouchEvent()方法的值,判断是否拦截。如果mFirstTouchTarget != null,即已经发生过MotionEvent.ACTION_DOWN,并且该事件已经有ViewGroup的子View进行处理了,那么也进入判断,调用ViewGroup. onInterceptTouchEvent()方法的值,判断是否拦截。如果不是以上两种情况,即已经是MOVE或UP事件了,并且之前的事件没有对象进行处理,则设置成true,开始拦截接下来的所有事件。这也就解释了如果子View的onTouchEvent()方法返回false,那么接下来的一些列事件都不会交给他处理。如果VieGroup的onInterceptTouchEvent()第一次执行为true,则mFirstTouchTarget = null,则也会使得接下来不会调用onInterceptTouchEvent(),直接将拦截设置为true。
-
滑动冲突
1.外部拦截法
从父View着手,重写onInterceptTouchEvent方法,在父View需要拦截的时候拦截,不需要则不拦截返回false。其伪代码如下:
public boolean onInterceptTouchEvent(MotionEvent event) { boolean intercepted = false; int x = (int)event.getX(); int y = (int)event.getY(); switch (event.getAction()) { case MotionEvent.ACTION_DOWN: { intercepted = false; break; } case MotionEvent.ACTION_MOVE: { if (满足父容器的拦截要求) { intercepted = true; } else { intercepted = false; } break; } case MotionEvent.ACTION_UP: { intercepted = false; break; } default: break; } mLastXIntercept = x; mLastYIntercept = y; return intercepted; } 在这里,首先down事件父容器必须返回false ,因为若是返回true,也就是拦截了down事件,那么后续的move和up事件就都会传递给父容器,子元素就没有机会处理事件了。move事件,根据需要决定是否拦截,如果父容器需要拦截就返回false,否则返回true。其次是up事件也返回false,一是因为up事件对父容器没什么意义,其次是因为若事件是子元素处理的,却没有收到up事件会让子元素的onClick事件无法触发。
2.内部拦截法
所有事件都传递给子元素,如果子元素需要就消耗掉,不需要就交给父元素处理,需要子元素配合requestDisallowInterceptTouchEvent方法才能正常工作;此外,父元素需要默认拦截除ACTION_DOWN以外的事件(下文的Flag作用与他相反,一旦子view设置,ViewGroup无法拦截除ACTION_DOWN以外的事件),这样子元素调用parent.requestDisallowInterceptTouchEvent(false)方法时,父元素才能继续拦截需要的事件。
因为viewGroup在分发事件时,如果是down事件,会重置这个标记,那么子view设置的就会无效(标记在requestDisallowInterceptTouchEvent方法中设置),down事件不受FLAG_DISALLOW_INTERCEPT这个标记的控制,所以一旦父容器拦截down事件,那么所有事件都无法传递到子元素去。
@Override public boolean dispatchTouchEvent(MotionEvent event) { int x = (int) event.getX(); int y = (int) event.getY(); switch (event.getAction()) { case MotionEvent.ACTION_DOWN: { parent.requestDisallowInterceptTouchEvent(true); break; } case MotionEvent.ACTION_MOVE: { int deltaX = x - mLastX; int deltaY = y - mLastY; if (父容器需要此类点击事件) { parent.requestDisallowInterceptTouchEvent(false); } break; } case MotionEvent.ACTION_UP: { break; } default: break; } mLastX = x; mLastY = y; return super.dispatchTouchEvent(event); }
然后修改父容器的onInterceptTouchEvent方法:
@Override public boolean onInterceptTouchEvent(MotionEvent event) { int action = event.getAction(); if (action == MotionEvent.ACTION_DOWN) { return false; } else { return true; } }
-
MotionEvent事件
-
MotionEvent.ACTION_DOWN
当屏幕检测到第一个触点按下之后就会触发到这个事件。
-
MotionEvent.ACTION_MOVE
当触点在屏幕上移动时触发,触点在屏幕上停留也是会触发的,主要是由于它的灵敏度很高,而我们的手指又不可能完全静止(即使我们感觉不到移动,但其实我们的手指也在不停地抖动)。
-
MotionEvent.ACTION_UP
当最后一个触点松开时被触发。
-
MotionEvent.ACTION_CANCEL
不是由用户直接触发,有系统再需要的时候触发,例如当父view通过使函数onInterceptTouchEvent()返回true,拦截了事件,也就是从子view拿回处理事件的控制权时,就会给子view发一个ACTION_CANCEL事件,这里了view就再也不会收到事件了。可以将其视为ACTION_UP事件对待。
-
-
-
View的绘制流程,如何自定义View
View的绘制是从上往下一层层迭代下来的。DecorView-->ViewGroup(--->ViewGroup)-->View ,按照这个流程从上往下,从ViewRoot的performTraversals方法开始的,它经过measure、layout和draw三个过程才能最终将一个View绘制出来,其中measure用来测量View的宽和高(几乎所有的情况下测量的宽高就是View最终的宽高),layout来确定View在父容器的放置位置(View的4个顶点和实际的View宽高),而draw则负责将View绘制在屏幕上。
理解View的测量过程,我们需要先理解一下MeasureSpec。MeasureSpec(32位int值)由两部分组成,一部分是测量模式(SpecMode高2位),另一部分是测量的尺寸大小(SpecSize低30位)。
SpecMode有三类:
- UNSPECIFIED :不对View进行任何限制,要多大给多大,一般用于系统内部
- EXACTLY:对应LayoutParams中的match_parent和具体数值这两种模式。检测到View所需要的精确大小,这时候View的最终大小就是SpecSize所指定的值,
- AT_MOST :对应LayoutParams中的wrap_content。View的大小不能大于父容器的大小。
那么MeasureSpec又是如何确定的?
对于顶级View,也就是DecorView,其确定是通过屏幕的大小和自身的布局参数LayoutParams确定的。那么对于View,其MeasureSpec由父布局的MeasureSpec和自身的布局参数LayoutParams来决定。
- 当View采用固定宽/高时(即设置固定的dp/px),不管父容器的MeasureSpec是什么,View的MeasureSpec都是EXACTLY模式,并且大小遵循Layoutparams的大小。
- 当View的宽/高是match_parents时,如果父容器的模式是精准模式,那么View也是精准模式并且其大小是父容器的剩余空间;如果父容器是最大模式那么View也是最大模式并且其大小不会超过父容器的剩余空间
- 当View的宽/高是wrap_content时,View的MeasureSpec都是AT_MOST模式并且其大小不能超过父容器的剩余空间。
- UNSPECIFIED主要用于系统内部多次Measure的情况,不太需要关注
measure:
-
View的measure过程由其measure()方法完成,measure()方法是final类型的,子类不能重写。在View的measure()方法中会去调用View的onMeasure()方法来完成测量。有两个重要的方法如下:
注意:直接继承View的自定义控件需要重写onMeasure方法并设置wrap_content时的自身大小,否则在布局中使用wrap_content就相当于使用match_parent.
解决:只需要给View指定一个默认的内部宽高(mWidth和mHeight),并在wrap_content时设置此宽高即可
-
ViewGroup的measure过程
从ViewGroup至子View、自上而下遍历进行(即树形递归),通过计算整个ViewGroup中各个View的属性,从而最终确定整个ViewGroup的属性。
单一View的measure过程对onMeasure()有统一的实现,但ViewGroup的measure过程是没有的,因为ViewGroup是一个抽象类,它的子类如:LinearLayout、RelativeLayout、自定义ViewGroup子类等具备不同的布局特性,这导致它们的子View测量方法各有不同,所以onMeasure()的实现也会有所不同,无法对onMeasure()作统一实现,所以其测量过程的onMeasure方法由各个子类去具体实现。
注意:
针对Measure流程,自定义ViewGroup的关键在于:根据需求复写onMeasure(),从而实现子View的测量逻辑。复写onMeasure()的步骤主要分为三步:
- 遍历所有子View及测量:measureChildren()
- 合并所有子View的尺寸大小,最终得到ViewGroup父视图的测量值:需自定义实现
- 存储测量后View宽/高的值:setMeasuredDimension()
layout:
测量完View大小后,就需要将View布局在Window中,View的布局主要通过确定上下左右四个点来确定的。
其中布局也是自上而下,不同的是ViewGroup先在layout()中确定自己的布局,然后在onLayout()方法中再调用子View的layout()方法,让子View布局。相反在Measure过程中,ViewGroup一般是先测量子View的大小,然后再确定自身的大小。
注意:onLayout()用来确定子View的布局,onLayout()是一个可继承的空方法,具体实现和具体布局有关。
draw:
View的绘制过程遵循如下几步:
绘制背景 background.draw(canvas)
绘制自己(onDraw)
绘制Children(dispatchDraw)
绘制装饰(onDrawScrollBars)
无论是ViewGroup还是单一的View,都需要实现这套流程,不同的是,在ViewGroup中,实现了 dispatchDraw()方法,而在单一子View中不需要实现该方法。自定义View一般要重写onDraw()方法,在其中绘制不同的样式。
-
requestLayout和invalide的区别
https://blog.csdn.net/a553181867/article/details/51583060
-
requestLayout
简要说明:
子View调用requestLayout方法,会标记当前View及父容器,同时逐层向上提交,直到ViewRootImpl处理该事件,ViewRootImpl会调用三大流程,从measure开始,对于每一个含有标记位的view及其子View都会进行测量、布局、绘制。
详细说明:
在requestLayout方法中,首先先判断当前View树是否正在布局流程,接着为当前子View设置标记位,该标记位的作用就是标记了当前的View是需要进行重新布局的,接着调用mParent.requestLayout方法,这个十分重要,因为这里是向父容器请求布局,即调用父容器的requestLayout方法,为父容器添加PFLAG_FORCE_LAYOUT标记位,而父容器又会调用它的父容器的requestLayout方法,即requestLayout事件层层向上传递,直到DecorView,即根View,而根View又会传递给ViewRootImpl,也即是说子View的requestLayout事件,最终会被ViewRootImpl接收并得到处理。纵观这个向上传递的流程,其实是采用了责任链模式,即不断向上传递该事件,直到找到能处理该事件的上级,在这里,只有ViewRootImpl能够处理requestLayout事件。
public void requestLayout() { if (mMeasureCache != null) mMeasureCache.clear(); if (mAttachInfo != null && mAttachInfo.mViewRequestingLayout == null) { // Only trigger request-during-layout logic if this is the view requesting it, // not the views in its parent hierarchy ViewRootImpl viewRoot = getViewRootImpl(); if (viewRoot != null && viewRoot.isInLayout()) { if (!viewRoot.requestLayoutDuringLayout(this)) { return; } } mAttachInfo.mViewRequestingLayout = this; } //为当前view设置标记位 PFLAG_FORCE_LAYOUT mPrivateFlags |= PFLAG_FORCE_LAYOUT; mPrivateFlags |= PFLAG_INVALIDATED; if (mParent != null && !mParent.isLayoutRequested()) { //向父容器请求布局 mParent.requestLayout(); } if (mAttachInfo != null && mAttachInfo.mViewRequestingLayout == this) { mAttachInfo.mViewRequestingLayout = null; } }
在requestLayout方法中,首先先判断当前View树是否正在布局流程,接着为当前子View设置标记位,该标记位的作用就是标记了当前的View是需要进行重新布局的,接着调用mParent.requestLayout方法,这个十分重要,因为这里是向父容器请求布局,即调用父容器的requestLayout方法,为父容器添加PFLAG_FORCE_LAYOUT标记位,而父容器又会调用它的父容器的requestLayout方法,即requestLayout事件层层向上传递,直到DecorView,即根View,而根View又会传递给ViewRootImpl,也即是说子View的requestLayout事件,最终会被ViewRootImpl接收并得到处理。纵观这个向上传递的流程,其实是采用了责任链模式,即不断向上传递该事件,直到找到能处理该事件的上级,在这里,只有ViewRootImpl能够处理requestLayout事件。
-
invalidate
该方法的调用会引起View树的重绘,常用于内部调用(比如 setVisiblity())或者需要刷新界面的时候,需要在主线程(即UI线程)中调用该方法。
当子View调用了 invalidate()方法后,会为该View添加一个标记位,同时不断向父容器请求刷新,父容器通过计算得出自身需要重绘的区域,直到传递到ViewRootImpl中,最终触发 performTraversals() 方法,进行开始View树重绘流程(由于没有添加measure和layout的标记位,因此measure、layout流程不会执行,而是直接从draw流程开始,只绘制需要重绘的视图)。
-
postInvalidate
这个方法与invalidate方法的作用是一样的,都是使View树重绘,但两者的使用条件不同,postInvalidate是在非UI线程中调用,invalidate则是在UI线程中调用。
在子线程当中刷新;其实最终调用的就是 invalidate(),原理依然 是通过工作线程向主线程发送消息这一机制。
大致区别:
一般来说,如果View确定自身不再适合当前区域,比如说它的LayoutParams发生了改变,需要父布局对其进行重新测量、布局、绘制这三个流程,往往使用requestLayout。而invalidate则是刷新当前View,使当前View进行重绘,不会进行测量、布局流程,因此如果View只需要重绘而不需要测量,布局的时候,使用invalidate方法往往比requestLayout方法更高效。
-
-
简析Window、Activity、DecorView以及ViewRoot之间的错综关系
-
window
Window 是一个抽象类,它的具体实现是PhoneWindow,视图的承载器,用于控制视图。
-
Activity
Activity并不负责视图控制,它只是控制生命周期和处理事件。
-
DecorView
DecorView是FrameLayout的子类,它可以被认为是Android视图树的根节点视图,顶级View
-
ViewRoot
所有View的绘制以及事件分发等交互都是通过它来执行或传递的。
一个Activity包含一个Window对象,这个对象是由PhoneWindow来实现的。PhoneWindow将DecorView作为整个应用窗口的根View,而这个DercorView又将屏幕分为2个区域,一个TitleView,一个ContentView,我们平时做的应用所写的布局正是展示在ContetView中。
Activity 创建时通过 attach() 初始化了一个 Window 也就是PhoneWindow,一个 PhoneWindow 持有一个 DecorView 的实例,DecorView 本身是一个 FrameLayout,继承于 View,Activty 通过setContentView 将 xml 布局控件不断 addView()添加到 View 中,最终显示到 Window 于我们交互;
ViewRoot 对应于 ViewRootImpl 类,它是连接 WindowManager 和 DecorView 的纽带, View 的三大流程均是通过 ViewRoot 来完成的。在 ActivityThread 中,当 Activity 对象被创建完毕后,会将 DecorView 添加到 Window 中,同时会创建 ViewRootImpl 对象,并将 ViewRootImpl 对象和 DecorView 建立关联。
-
-
RecycleView和ListView的区别?RecycleView原理?复用机制?
https://juejin.cn/post/6844903778303344647#comment
https://cloud.tencent.com/developer/article/1799783?from=article.detail.1706419
-
布局上
-
listview:
布局比较单一,只支持竖直方向滑动
-
recyclerview:三种布局
- 线性布局,这个和listview相似 ,实现横向/纵向列表方向的item
- 网格布局,可以指定item的数量
- 瀑布流布局,可以指定列表方向,也可以指定同方向的item数量
-
-
局部刷新
-
listview:
listview中通常刷新数据 notifyDataSetChanged() ,这种刷新是全局刷新的,每一个item的数据都会重新加载一次,这样很消耗资源,在一些需要频繁更新数据的场景,比如淘宝实时更新的界面,listview实现会很鸡肋。
-
recyclerview:
可以通过 notifyItemChanged() 来实现局部刷新 。
ps:不过如果要在ListView实现局部刷新,依然是可以实现的,当一个item数据刷新时,我们可以在Adapter中,实现一个onItemChanged()方法,在方法里面获取到这个item的position(可以通过getFirstVisiblePosition()),然后调用getView()方法来刷新这个item的数据。
-
-
item view的重用
-
listview:
默认每次加载一个新的item创建一个新view,引起内存增加,不过可以通过判断 convertView 是否为空来重用view。convertView 不为空,则不会产生新的条目, 屏幕上始终是一开始生成的那几个条目
-
recyclerview:
默认实现重用view, RecyclerView复用item全部搞定,不需要像ListView那样setTag()与getTag();
-
-
ViewHolder
-
listview:
viewHolder需要自定义,如果用getview去获取控件,则每次调用getview都要通过 findViewById 去获取控件, 如果控件个数过多,会严重影响性能 ,因为findViewById相对比较耗时,所以我们需要创建自定义viewHolder,通过getTag和setTag直接获取view。
-
recyclerview:
继承recyclerView.ViewHolder,默认需要重写viewHodler,使用已经封装好的
-
-
嵌套滑动机制
Android 5.0推出了嵌套滑动机制,在之前,一旦子View处理了触摸事件,父View就没有机会再处理这次的触摸事件,而嵌套滑动机制解决了这个问题。
为了支持嵌套滑动,子View必须实现NestedScrollingChild接口,父View必须实现NestedScrollingParent接口。
NestedScrollingChild接口,而CoordinatorLayout实现了NestedScrollingParent接口,上图是实现CoordinatorLayout嵌套RecyclerView的效果。
listview: ListView 并不支持嵌套滚动机制
-
空数据处理
- ListView 提供了 setEmptyView 这个 API 来让我们处理 Adapter 中数据为空的情况,只需轻轻一 set 就能搞定一切。
- 而 RecyclerView 并没有提供此类 API。
-
Recyclerview的四级缓存
Recycleview有四级缓存,分别是mAttachedScrap(屏幕内),mCacheViews(屏幕外),mViewCacheExtension(自定义缓存),mRecyclerPool(缓存池)
- mAttachedScrap(屏幕内),用于屏幕内itemview快速重用,不需要重新createView和bindView
- mCacheViews(屏幕外),保存最近移出屏幕的ViewHolder,包含数据和position信息,复用时必须是相同位置的ViewHolder才能复用,应用场景在那些需要来回滑动的列表中,当往回滑动时,能直接复用ViewHolder数据,不需要重新bindView。
- mViewCacheExtension(自定义缓存),不直接使用,需要用户自定义实现,默认不实现。
- mRecyclerPool(缓存池),当cacheView满了后或者adapter被更换,将cacheView中移出的ViewHolder放到Pool中,放之前会把ViewHolder数据清除掉,所以复用时需要重新bindView。
四级缓存按照顺序需要依次读取。所以完整缓存流程是:
-
保存缓存流程:
- 插入或是删除itemView时,先把屏幕内的ViewHolder保存至AttachedScrap中
- 滑动屏幕的时候,先消失的itemview会保存到CacheView,CacheView大小默认是2,超过数量的话按照先入先出原则,移出头部的itemview保存到RecyclerPool缓存池(如果有自定义缓存就会保存到自定义缓存里),RecyclerPool缓存池会按照itemview的itemtype进行保存,每个itemTyep缓存个数为5个,超过就会被回收。
-
获取缓存流程:
- AttachedScrap中获取,通过pos匹配holder——>获取失败,从CacheView中获取,也是通过pos获取holder缓存 ——>获取失败,从自定义缓存中获取缓存——>获取失败,从mRecyclerPool中获取 ——>获取失败,重新创建viewholder——createViewHolder并bindview。
需要注意的是,如果从缓存池找到缓存,还需要重新bindview。
Recycleview自带了一些布局变化的动画效果,也可以通过自定义ItemAnimator类实现自定义动画效果
-
-
Context,Context有哪几种?继承关系?一个App有多少个context
参考文章:https://blog.csdn.net/guolin_blog/article/details/47028975
Context的继承结构还是稍微有点复杂的,直系子类有两个,一个是ContextWrapper,一个是ContextImpl。那么从名字上就可以看出,ContextWrapper是上下文功能的封装类,而ContextImpl则是上下文功能的实现类。而ContextWrapper又有三个直接的子类,ContextThemeWrapper、Service和Application。其中,ContextThemeWrapper是一个带主题的封装类,而它有一个直接子类就是Activity。
由此得出结论,Context一共有三种类型,分别是Application、Activity和Service。这三个类虽然分别各种承担着不同的作用,但它们都属于Context的一种,而它们具体Context的功能则是由ContextImpl类去实现的。Context实现功能:
弹出Toast、启动Activity、启动Service、发送广播、操作数据库等都需要用到Context。由于Context的具体能力是由ContextImpl类去实现的,因此在绝大多数场景下,Activity、Service和Application这三种类型的Context都是可以通用的。不过有几种场景比较特殊,比如启动Activity,还有弹出Dialog。出于安全原因的考虑,Android是不允许Activity或Dialog凭空出现的,一个Activity的启动必须要建立在另一个Activity的基础之上,也就是以此形成的返回栈。而Dialog则必须在一个Activity上面弹出(除非是System Alert类型的Dialog),因此在这种场景下,我们只能使用Activity类型的Context,否则将会出错。
Context数量:
Context数量 = Activity数量 + Service数量 + 1
上面的1代表着Application的数量,因为一个应用程序中可以有多个Activity和多个Service,但是只能有一个Application。
getApplication()方法的语义性非常强,一看就知道是用来获取Application实例的,但是这个方法只有在Activity和Service中才能调用的到。那么也许在绝大多数情况下我们都是在Activity或者Service中使用Application的,但是如果在一些其它的场景,比如BroadcastReceiver中也想获得Application的实例,这时就可以借助getApplicationContext()方法。getBaseContext()方法得到的是一个ContextImpl对象,像Application、Activity这样的类其实并不会去具体实现Context的功能,而仅仅是做了一层接口封装而已,Context的具体功能都是由ContextImpl类去完成的。
Application的使用细节
错误写法:
public class MyApplication extends Application { private static MyApplication app; public static MyApplication getInstance() { if (app == null) { app = new MyApplication(); } return app; }
因为我们知道Application是属于系统组件,系统组件的实例是要由系统来去创建的,如果这里我们自己去new一个MyApplication的实例,它就只是一个普通的Java对象而已,而不具备任何Context的能力。
正确写法:
public class MyApplication extends Application { private static MyApplication app; public static MyApplication getInstance() { return app; } @Override public void onCreate() { super.onCreate(); app = this; } }
-
Android的数据存储方式?
-
SharedPreferences
一种轻型的数据存储方式,本质是基于 XML 文件存储的 key-value 键值对数据,通常用来存储一些简单的配置信息(如应用程序的各种配置信息)
-
SQLite 数据库存储
一种轻量级嵌入式数据库引擎,它的运算速度非常快,占用资源很少,常用来存储大量复杂的
关系数据 -
ContentProvider
四大组件之一,用于数据的存储和共享,不仅可以让不同应用程序之间进行数据共享,还可以选择只对哪一部分数据进行共享,可保证程序中的隐私数据不会有泄漏风险
-
File 文件存储
写入和读取文件的方法和Java 中实现 I/O的程序一样
-
网络存储
主要在远程的服务器中存储相关数据,用户操作的相关数据可以同步到服务器上
-
-
SharedPreferences commit和await区别,SharedPreferences的缺点?
-
commit和await区别
- apply() 没有返回值而 commit() 可以通过 boolean 类型的返回值表明修改是否提
- apply() 是将修改数据原子提交到内存,然后异步地真正写入硬件磁盘, 而 commit() 是同步地写入硬件磁盘;因此,在多个并发的提交 commit() 的时候,他们会等待正在处理的 commit() 保存到磁盘后再进行下一步操作,这样就降低了效率。而 apply()只是原子地提交到内容,后续的提交会覆盖之前的提交到内存的内容,只有最后一次提交的内容会被写入磁盘。
-
SharedPreferences缺点
SharedPreferences是一个特例,它通过键值对的方式来存储数据,在底层实现上它采用XML文件来存储键值对,虽然从本质上来讲,它也是文件的一种,但是由于系统对它的读写有一定的缓存策略,即在内存中会有一份SharedPreferences文件的缓存,因此在多进程模式下、系统对它的读写就变的不可靠,当面对高并发读写访问的时候,有很大几率会丢失数据,因此,不建议在进程间通信中使用SharedPreferences。
源码分析:https://www.jianshu.com/p/cba020b1fbc6
-
-
MVC / MVP / MVVM 架构模式
Android架构,即为开发Android时使用的架构。Android的开发一般分为三部分:UI逻辑,业务逻辑和数据操作逻辑。
Android架构,就是为了更好地协调这三者的关系。达到:
- 各模块高内聚低耦合的状态,方便进行团队分工合作开发。
- 代码思路清晰,提高代码的可维护性与可测试性。
- 减少样板代码,提高开发效率,减少开发错误。
MVC
Android上的MVC架构我认为是来源于web开发的SpringMVC,MVC全名为Model-View-Controller,图解如下:
View:负责与用户交汇,显示界面。
Controller:负责接收来自view的请求,处理业务逻辑。
Model:负责数据逻辑,网络请求数据以及本地数据库操作数据等。
在MVC架构中,Controller是业务的主要承载者,几乎所有的业务逻辑都在Controller中进行编写。而View主要负责UI逻辑,而Model是数据逻辑,彼此分工。
MVC的本质就是按照UI逻辑、业务逻辑、数据操作逻辑不同的职责分三大模块,彼此分工。
在Android中,View一般使用xml进行编写,但xml的能力不全面,需要Activity进行一些UI逻辑的编写,因而MVC中的V即为xml+Activity。Model数据层,在Android中负责网络请求和数据库操作,并向外暴露接口。Controller是争议比较多的写法:一种是直接把Activity当成Controller;一种是独立出Controller类,进行逻辑分离。比较符合MVC思想的笔者认为是后者。因为前者直接在Activity中进行书写业务逻辑就会和UI逻辑混合在一起了,达不到模块分工的效果。
优点:简单。他不需要写很多的代码来让代码解耦,这在小型项目非常有用。小型项目总体的代码就不多,所以这样可以提高开发效率。
缺点:
几乎所有的业务逻辑代码都在Controller中进行,会导致非常臃肿,降低项目的可测试性与可维护性。
View直接持有Controller和Model实例,不同职责的代码进行耦合,导致代码耦合性高,模块分工不清晰。
改进方向:
对模块进行更加彻底的分离,不要让View和Model直接联系。对Controller进行减压。
MVP
相比MVC,MVP的更加的完善。MVP全名是Model-View-Presenter。图解如下:
- View:UI模块,负责界面显示和与用户交汇。
- Presenter:负责业务逻辑,起着连接View和Model桥梁的作用。
- Model:专注于数据操作逻辑。
MVP 和 MVC 的区别很明显就在这个Presenter中。为了解决MVC中代码的耦合严重性,把业务逻辑都抽离到了Presenter中。这样View和Model完全被隔离,实现了单向依赖,大大减少了耦合度。View和Prensenter之间通过接口来通信,只要定义好接口,那么团队可以合作同时开发不同的模块,同时不同的模块也可以进行独立测试。也因各模块独立了,所以要只要符合接口规范,即可做到动态更换模块而不需要修改其他的模块。
在Android中,需要让Activity提供控件的更新接口,Prensenter提供业务逻辑接口,Activity持有Presenter的实例,Presenter持有Activity的弱引用(不用直接引用是为了避免内存泄露),Activity直接调用Presenter的方法更新界面,Presenter去Model获取数据之后,通过View的接口更新View。
不同的View可以通过实现相同的接口来共享Prensenter。Prensenter也可以通过实现接口来实现动态更换逻辑。Model是完全独立开发的,向外暴露的方法参数中含有callBack参数,可以直接调用callBack进行回调。
优点:
- MVP通过模块职责分工,抽离业务逻辑,降低代码的耦合性
- 实现模块间的单向依赖,代码思路清晰,提高可维护性
- 模块间通过接口进行通信,降低了模块间的耦合度,可以实现不同模块独立开发或动态更换
缺点:
- 过度设计导致接口过多,编写大量的代码来实现模块解耦,降低了开发效率
- 并没有彻底进行解耦,Prensenter需要同时处理UI逻辑和业务逻辑,Prensenter臃肿
MVVM
MVVM和上面两种架构模式一样都是一种架构思想,只是谷歌推出了jetpack架构组件来让我们更好的使用这种架构模式。
MVVM,全名为Model-View-ViewModel。
- View:和前面的MVP、MVC中的View一样,负责UI界面的显示以及与用户的交汇。
- Model:同样是负责网络数据获取或者本地数据库数据获取。
- ViewModel:负责存储View的数据映像以及业务逻辑。
MVVM的View和Model和前面的两种架构模式是差不多的,重点在ViewModel。ViewModel通过将数据和View进行绑定,修改数据会直接反映到View上,通过数据驱动型思想,彻底把MVP中的Presenter的UI操作逻辑给去掉了。而ViewModel是绑定于单独的View的,也就不需要进行编写接口了。但ViewModel中依旧有很多的业务逻辑,但是因为把View和数据进行绑定,这样可以让View和业务彻底的解耦了。View可以专注于UI操作,而ViewModel可以专注于业务操作。因而MVVM通过数据驱动型思想,彻底把业务和UI逻辑进行解耦,各模块分工职责明确。
缺点:
MVVM的ViewModel依旧很臃肿。
MVVM需要学习数据绑定框架,具有一定的上手难度。
适合android开发的MVVM架构模式:
简单的解析:
View对应的就是Activity和Fragment,在这里进行UI操作。
ViewModel中包含了LiveData,这是一种可观察数据类型框架。View通过向LiveData注册观察者,当LiveData发生改变时,就会直接调用观察者的逻辑把数据更新到View上。
ViewModel完全不需要关心UI操作,只需要专注于数据与业务操作。
Repository代表了Model层,Repository对ViewModel进行了减压,i把业务操作般到了Repository中,避免了ViewModel臃肿。
Repository对请求进行判断是要到本地数据库获取还是网络请求获取分别调用不同的模块。