一、Handler机制
创建一个Handler
//提示已过时
Handler handler = new Handler();
Handler handler = new Handler(Looper.myLooper());
隐式指定Looper
的Handler
初始化方法已被Android 11报过时,根据注释,是由于不指定Looper
在一些场景下会导致任务丢失或程序崩溃,比如没有Looper
的线程。
public static @Nullable Looper myLooper() {
return sThreadLocal.get();
}
下面寻找这个Looper
是在哪里初始化的
在ActivityThread.java#main
中
public static void main(String[] args) {
...
Looper.prepareMainLooper();
...
Looper.loop();
}
Looper.java
public static void prepareMainLooper() {
prepare(false);
synchronized (Looper.class) {
if (sMainLooper != null) {
throw new IllegalStateException("The main Looper has already been prepared.");
}
sMainLooper = myLooper();
}
}
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));
}
public static @Nullable Looper myLooper() {
return sThreadLocal.get();
}
根据ThreadLocal
原理,调用Looper.prepare(boolean quitAllowed)
时将该<ThreadLocal<Looper>,Looper>
键值对设置到了当前线程的成员变量ThreadLocal.ThreadLocalMap threadLocals
中,从而保证了每个线程对应唯一一个Looper
,且不能被重复初始化,否则将抛出异常。而且Looper
对象在初始化时也初始化了MessageQueue
,并将当前线程保存在成员变量中。
private Looper(boolean quitAllowed) {
mQueue = new MessageQueue(quitAllowed);
mThread = Thread.currentThread();
}
以上初步实现了主线程对应sMainLooper
,同时对应唯一一个MessageQueue
。
下面分析Looper
、MessageQueue
、Handler
如何运转起来
public static void loop() {
final Looper me = myLooper();
...
final MessageQueue queue = me.mQueue;
...
//死循环
for (;;) {
Message msg = queue.next();
...
try {
//将消息分发给相应Handler处理
msg.target.dispatchMessage(msg);
...
}
}
...
}
下面看Handler
处理消息的过程
从sendMessage()
、postDelayed()
、post()
都会调用到enqueueMessage
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);
}
下面看MessageQueue#enqueueMessage
:
boolean enqueueMessage(Message msg, long when) {
...
synchronized (this) {
...
Message p = mMessages;
boolean needWake;
if (p == null || when == 0 || when < p.when) {
// New head, wake up the event queue if blocked.
msg.next = p;
mMessages = msg;
needWake = mBlocked;
} else {
...
Message prev;
for (;;) {
prev = p;
p = p.next;
if (p == null || when < p.when) {
break;
}
if (needWake && p.isAsynchronous()) {
needWake = false;
}
}
msg.next = p; // invariant: p == prev.next
prev.next = msg;
}
...//唤醒线程本地方法
}
}
可以看到,消息入队时加了锁,保证了多个Handler
向MessageQueue
添加数据时的线程安全。添加消息首先判断队列中是否有消息,没有消息的时候需要对线程进行唤醒。如果不需要唤醒,则按照延迟时间入队。至于时间的判定,来自于Handler#sendMessageDelayed
public final boolean sendMessageDelayed(@NonNull Message msg, long delayMillis) {
if (delayMillis < 0) {
delayMillis = 0;
}
return sendMessageAtTime(msg, SystemClock.uptimeMillis() + delayMillis);
}
这个时间在SystemClock.java
中有说明:
/**
* Returns milliseconds since boot, not counting time spent in deep sleep.
*
* @return milliseconds of non-sleep uptime since boot.
*/
@CriticalNative
native public static long uptimeMillis();
根据注释,它获取的是系统从开机开始的时间,除去休眠时间。
相关面试问题:
- Linux的epoll机制
- 一个线程有几个Handler
- 一个线程有几个Looper?如何保证?
- 子线程可以创建Handler吗
- 多个Handler往MessageQueue中添加数据,如何确保线程安全
- Looper.loop()为什么不会阻塞主线程
- Message的数据结构
- Handler内存泄漏场景有哪些,如何避免
二、View绘制流程
Activity
通过attach
和Window
关联起来,Window
通过setContentView
和View
关联起来
从setContentView
开始看绘制流程
public void setContentView(@LayoutRes int layoutResID) {
getWindow().setContentView(layoutResID);
initWindowDecorActionBar();
}
PhoneWindow#setContentView
public void setContentView(int layoutResID) {
if (mContentParent == null) {
installDecor();
}
...
}
private void installDecor() {
if (mDecor == null) {
mDecor = generateDecor(-1);
...
}
...
}
protected DecorView generateDecor(int featureId) {
...
return new DecorView(context, featureId, this, getAttributes());
}
PhoneWindow
类中包含DecorView
成员变量,由此将Window
和View
联系起来
下面看布局是怎么被设置进去的:
private void installDecor() {
...//DecorView初始化
if (mContentParent == null) {
mContentParent = generateLayout(mDecor);
...
}
...
}
protected ViewGroup generateLayout(DecorView decor) {
...
int layoutResource;//系统加载布局资源id
int features = getLocalFeatures();
...
mDecor.startChanging();
mDecor.onResourcesLoaded(mLayoutInflater, layoutResource);
}
DecorView.java
void onResourcesLoaded(LayoutInflater inflater, int layoutResource) {
...
final View root = inflater.inflate(layoutResource, null);
...
addView(root, 0, new ViewGroup.LayoutParams(MATCH_PARENT, MATCH_PARENT));
...
}
根据设置的feature
参数,按照类型加载系统布局资源文件,然后通过addView
加载到自身DecorView
中
然后回到PhoneWindow#setContentView
:
mLayoutInflater.inflate(layoutResID, mContentParent);
到此自己的资源文件已经加载到mContentParent
中,构成了一个完整的控件树。
- 更新UI的线程检查
public ViewRootImpl(Context context, Display display, IWindowSession session,
boolean useSfChoreographer) {
...
mThread = Thread.currentThread();
...
}
void checkThread() {
if (mThread != Thread.currentThread()) {
throw new CalledFromWrongThreadException(
"Only the original thread that created a view hierarchy can touch its views.");
}
}
由此可见,检查的并非是主线程,而是ViewRootImpl
创建时的线程,下面寻找ViewRootImpl
是在哪里创建出来的。
通过打断点查看调用栈,能够找到调用链:
ActivityThread.java
@Override
public void handleResumeActivity(IBinder token, boolean finalStateRequest, boolean isForward,
String reason) {
...
final ActivityClientRecord r = performResumeActivity(token, finalStateRequest, reason);//执行Activity的onResume方法
...
wm.addView(decor, l);
...
}
WindowManagerImpl.java
private final WindowManagerGlobal mGlobal = WindowManagerGlobal.getInstance();
public void addView(@NonNull View view, @NonNull ViewGroup.LayoutParams params) {
applyDefaultToken(params);
mGlobal.addView(view, params, mContext.getDisplayNoVerify(), mParentWindow,
mContext.getUserId());
}
WindowManagerGlobal.java
public void addView(View view, ViewGroup.LayoutParams params,
Display display, Window parentWindow, int userId) {
...
root = new ViewRootImpl(view.getContext(), display);
....
root.setView(view, wparams, panelParentView, userId);//此时也会将ViewRootImpl设置为DecorView的parent
...
}
每一个Activity
都持有一个WindowManagerImpl
对象,操作都会被转发给全局只有一个的WindowManagerGlobal
对象。
由代码执行过程可以看出,设置的mThread
一定是主线程。
- 开始绘制
ViewRootImpl#setView
:
public void setView(View view, WindowManager.LayoutParams attrs, View panelParentView,
int userId) {
synchronized (this) {
...
requestLayout();//开始绘制第一帧,执行测量布局绘制操作
...
}
...
view.assignParent(this);//将ViewRootImpl设置为DecorView的父View
}
public void requestLayout() {
if (!mHandlingLayoutInLayoutRequest) {
checkThread();
mLayoutRequested = true;
scheduleTraversals();
}
}
void scheduleTraversals() {
if (!mTraversalScheduled) {
...
mChoreographer.postCallback(
Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
...
}
}
绘制第一帧的操作是在ViewRootIImpl
中被调用,但调用时通过向主线程post
一个Runnable
实现的,所以绘制操作实际上是到下一次Loop
循环时才会执行,而不是立即执行的
- 如何在子线程更新UI
- 由于对于线程的检查是在
ViewRootImpl
初始化的时候做的,那么可以在子线程中创建一个ViewRootImpl
就可以了
new Thread(new Runnable() {
@Override
public void run() {
Looper.prepare();
//创建View
getWindowManager().addView(创建的View, new WindowManager.LayoutParams());
//更新UI操作
Looper.loop();
}
}).start();
- 也可以开启硬件加速,并设置控件为固定大小,绕过线程检查
在硬件加速的支持下,如果控件只是经常了 invalidate()
,而没有触发
requestLayout()
是不会 触发 ViewRootImpl#checkThread()
的。
常见问题
- 首次绘制触发时机以及
ViewRootImpl
创建时机
首次绘制是在 ActivityThread#handleResumeActivity
里触发的,调用链为WindowManagerImpl.addView
-> WindowManagerGlobal.addView
-> ViewRootImpl.setView
-> ViewRootImpl.requestLayout
,ViewRootImpl
也是在 ActivityThread.handleResumeActivity
里创建的。
ViewRootImpl
和DecorView
的关系
在 ViewRootImpl.setView
里,通过 DecorView.assignParent
把 ViewRootImpl
设置为 DecorView
的 parent。
三、触摸机制
getAction
和getActionMask
区别
getAction
获取到的是多点触控融合到一块的模糊信息,而getActionMask
获取的是准确信息,支持多点触控。两者之间没有性能差别。
- onTouchEvent源码分析
public boolean onTouchEvent(MotionEvent event) {
final float x = event.getX();
final float y = event.getY();
final int viewFlags = mViewFlags;
final int action = event.getAction();
//判断是否可点击
final boolean clickable = ((viewFlags & CLICKABLE) == CLICKABLE
|| (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)
|| (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE;
//如果被禁用,返回clickable的值,意味着如果被禁用,而clickable为true,那么这个事件被消费了,下面的view以及当前view的父view也收不到这个事件
if ((viewFlags & ENABLED_MASK) == DISABLED) {
if (action == MotionEvent.ACTION_UP && (mPrivateFlags & PFLAG_PRESSED) != 0) {
setPressed(false);
}
mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN;
return clickable;
}
//触摸代理,增加点击区域
if (mTouchDelegate != null) {
if (mTouchDelegate.onTouchEvent(event)) {
return true;
}
}
//tooltip为解释工具,与xml中设置的tooltiptext相对应
if (clickable || (viewFlags & TOOLTIP) == TOOLTIP) {
switch (action) {
case MotionEvent.ACTION_UP:
...
break;
case MotionEvent.ACTION_DOWN:
...
break;
case MotionEvent.ACTION_CANCEL:
...
break;
case MotionEvent.ACTION_MOVE:
...
break;
}
return true;
}
}
ACTION_DOWN分析
case MotionEvent.ACTION_DOWN:
//判断是否触摸屏幕(而非实体按键)
if (event.getSource() == InputDevice.SOURCE_TOUCHSCREEN) {
mPrivateFlags3 |= PFLAG3_FINGER_DOWN;
}
mHasPerformedLongPress = false;
//为了tooltip的设计
if (!clickable) {
checkForLongClick(
ViewConfiguration.getLongPressTimeout(),
x,
y,
TOUCH_GESTURE_CLASSIFIED__CLASSIFICATION__LONG_PRESS);
break;
}
//早期功能,鼠标右键点击
if (performButtonActionOnTouchDown(event)) {
break;
}
// 递归判断是否在滑动控件里
boolean isInScrollingContainer = isInScrollingContainer();
if (isInScrollingContainer) {
//不知道用户滑动还是点击,先记录下预点击状态
mPrivateFlags |= PFLAG_PREPRESSED;
if (mPendingCheckForTap == null) {
mPendingCheckForTap = new CheckForTap();
}
mPendingCheckForTap.x = event.getX();
mPendingCheckForTap.y = event.getY();
postDelayed(mPendingCheckForTap, ViewConfiguration.getTapTimeout());
} else {
// 如果不在滑动控件中,设置为按下状态
setPressed(true, x, y);
//设置长按等待器,长按等待时间为500ms
checkForLongClick(
ViewConfiguration.getLongPressTimeout(),
x,
y,
TOUCH_GESTURE_CLASSIFIED__CLASSIFICATION__LONG_PRESS);
}
break;
private final class CheckForTap implements Runnable {
public float x;
public float y;
@Override
public void run() {
//确认为点击事件而非滑动事件,预按下状态置空,设置为按下状态
mPrivateFlags &= ~PFLAG_PREPRESSED;
setPressed(true, x, y);
//由于前面预按下状态等待了100ms,所以这里长按等待器设置另外等待400ms,与非滑动控件内的长按等待时间保持一致
final long delay =
ViewConfiguration.getLongPressTimeout() - ViewConfiguration.getTapTimeout();
checkForLongClick(delay, x, y, TOUCH_GESTURE_CLASSIFIED__CLASSIFICATION__LONG_PRESS);
}
}
- 获取
action
用的是getAction
,说明源码只支持单点触控,不支持多点。 - 从代码逻辑中可以看出,
clickable
实际上控制是否可以消费事件,而disable
控制是否可点击 - 如果自定义一个不可滑动的
ViewGroup
,需要重写shouldDelayChildPressedState
使其返回false
,否则默认为可滑动控件,按下状态会延迟100ms
ACTION_DOWN
核心操作为,标记为按下状态,并设置一个长按等待器。
ACTION_MOVE分析
case MotionEvent.ACTION_MOVE:
if (clickable) {
//效果中心改变,例如波纹效果
drawableHotspotChanged(x, y);
}
//这段目前不太清楚
final int motionClassification = event.getClassification();
final boolean ambiguousGesture =
motionClassification == MotionEvent.CLASSIFICATION_AMBIGUOUS_GESTURE;
int touchSlop = mTouchSlop;
if (ambiguousGesture && hasPendingLongPressCallback()) {
if (!pointInView(x, y, touchSlop)) {
removeLongPressCallback();
long delay = (long) (ViewConfiguration.getLongPressTimeout()
* mAmbiguousGestureMultiplier);
delay -= event.getEventTime() - event.getDownTime();
checkForLongClick(
delay,
x,
y,
TOUCH_GESTURE_CLASSIFIED__CLASSIFICATION__LONG_PRESS);
}
touchSlop *= mAmbiguousGestureMultiplier;
}
//触摸出界,touchSlop为触摸边界
if (!pointInView(x, y, touchSlop)) {
removeTapCallback();//移除预按下计时器
removeLongPressCallback();//移除长按计时器
if ((mPrivateFlags & PFLAG_PRESSED) != 0) {//取消按下状态
setPressed(false);
}
mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN;
}
//大力按下,则直接触发长按
final boolean deepPress =
motionClassification == MotionEvent.CLASSIFICATION_DEEP_PRESS;
if (deepPress && hasPendingLongPressCallback()) {
removeLongPressCallback();
checkForLongClick(
0 /* send immediately */,
x,
y,
TOUCH_GESTURE_CLASSIFIED__CLASSIFICATION__DEEP_PRESS);
}
break;
ACTION_UP分析
case MotionEvent.ACTION_UP:
mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN;
if ((viewFlags & TOOLTIP) == TOOLTIP) {
//tooltip松手后延迟1.5s消失
handleTooltipUp();
}
if (!clickable) {
removeTapCallback();
removeLongPressCallback();
mInContextButtonPress = false;
mHasPerformedLongPress = false;
mIgnoreNextUpEvent = false;
break;
}
boolean prepressed = (mPrivateFlags & PFLAG_PREPRESSED) != 0;
//按下或预按下状态
if ((mPrivateFlags & PFLAG_PRESSED) != 0 || prepressed) {
boolean focusTaken = false;
//用于获取焦点,例如editText
if (isFocusable() && isFocusableInTouchMode() && !isFocused()) {
focusTaken = requestFocus();
}
if (prepressed) {
//预按下状态时抬起手指,设置为按下状态
setPressed(true, x, y);
}
if (!mHasPerformedLongPress && !mIgnoreNextUpEvent) {
removeLongPressCallback();
if (!focusTaken) {
if (mPerformClick == null) {
mPerformClick = new PerformClick();
}
if (!post(mPerformClick)) {
//触发点击事件
performClickInternal();
}
}
}
if (mUnsetPressedState == null) {
mUnsetPressedState = new UnsetPressedState();
}
//预按下时间内抬起手指,前面设置为了按下状态,由于在同一个循环中,所以人为加一个延迟64ms来触发用户可感知的点击事件
if (prepressed) {
postDelayed(mUnsetPressedState,
ViewConfiguration.getPressedStateDuration());
} else if (!post(mUnsetPressedState)) {
mUnsetPressedState.run();
}
removeTapCallback();
}
mIgnoreNextUpEvent = false;
break;
ACTION_UP阶段做的事情是,触发点击事件,处理预点击状态下按下抬起效果,以及消除之前各种状态和runnable
的设置。
ACTION_CANCEL只做了各种取消操作,这里不再单独分析。
- dispatchTouchEvent源码分析
ViewGroup
的dispatchTouchEvent
public boolean dispatchTouchEvent(MotionEvent ev) {
...
//开启一个新的事件序列,清空之前的状态
if (actionMasked == MotionEvent.ACTION_DOWN) {
cancelAndClearTouchTargets(ev);
resetTouchState();
}
//拦截处理
final boolean intercepted;
if (actionMasked == MotionEvent.ACTION_DOWN
|| mFirstTouchTarget != null) {
final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
if (!disallowIntercept) {
intercepted = onInterceptTouchEvent(ev);
ev.setAction(action); // restore action in case it was changed
} else {
intercepted = false;
}
} else {
intercepted = true;
}
...
if (!canceled && !intercepted) {
...
if (actionMasked == MotionEvent.ACTION_DOWN
|| (split && actionMasked == MotionEvent.ACTION_POINTER_DOWN)//split和分屏状态有关
|| actionMasked == MotionEvent.ACTION_HOVER_MOVE) {
...
newTouchTarget = getTouchTarget(child);//继续寻找能够消耗ACTION_DOWN事件的目标控件
}
}
}
private void resetTouchState() {
clearTouchTargets();
resetCancelNextUpFlag(this);
mGroupFlags &= ~FLAG_DISALLOW_INTERCEPT;
mNestedScrollAxes = SCROLL_AXIS_NONE;
}
- 新事件序列清理状态时,有一个
FLAG_DISALLOW_INTERCEPT
标志,它与requestDisallowInterceptTouchEvent
方法相对应,控制了此事件序列之间不会拦截事件,一般调用父view
的该方法,保证事件不被父view
拦截。 - 拦截逻辑的处理:在事件为
ACTION_DOWN
时直接触发,其他的事件会根据是否存在消耗ACTION_DOWN
事件的目标控件(即是否有mFirstTouchTarget
记录)而决定。
四、ContraintLayout属性详解
- 约束限制
限制控件大小不会超过约束范围
app:layout_constrainedWidth="true"
- 偏向
控制控件在垂直方向的30%的位置
app:layout_constraintVertical_bias="0.3"
- 约束链
在约束链第一个控件加上chainStyle
用来改变一组控件的布局方式
packed:打包
spread:扩散(默认)
spread_inside:内部扩散
app:layout_constraintVertical_chainStyle="packed"
- 宽高比
需要设置宽或者高为0dp,字母指定那条边通过计算得出
app:layout_constrainDimensionRatio="W,2:1"
- 百分比
指定控件相对于parent的百分比大小
app:layout_constraintWidth_percent="0.6"