Bootstrap

Android面试准备之Android基础

一、Handler机制

创建一个Handler

//提示已过时
Handler handler = new Handler();
Handler handler = new Handler(Looper.myLooper());

隐式指定LooperHandler初始化方法已被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
下面分析LooperMessageQueueHandler如何运转起来

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;
		}
		...//唤醒线程本地方法
	}
}

可以看到,消息入队时加了锁,保证了多个HandlerMessageQueue添加数据时的线程安全。添加消息首先判断队列中是否有消息,没有消息的时候需要对线程进行唤醒。如果不需要唤醒,则按照延迟时间入队。至于时间的判定,来自于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通过attachWindow关联起来,Window通过setContentViewView关联起来

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成员变量,由此将WindowView联系起来

下面看布局是怎么被设置进去的:

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
  1. 由于对于线程的检查是在ViewRootImpl初始化的时候做的,那么可以在子线程中创建一个ViewRootImpl就可以了
new Thread(new Runnable() {
            @Override
            public void run() {
                Looper.prepare();
                //创建View
                getWindowManager().addView(创建的View, new WindowManager.LayoutParams());
                //更新UI操作
                Looper.loop();
            }
        }).start();
  1. 也可以开启硬件加速,并设置控件为固定大小,绕过线程检查

在硬件加速的支持下,如果控件只是经常了 invalidate() ,而没有触发
requestLayout() 是不会 触发 ViewRootImpl#checkThread() 的。

常见问题

  • 首次绘制触发时机以及ViewRootImpl创建时机

首次绘制是在 ActivityThread#handleResumeActivity 里触发的,调用链为WindowManagerImpl.addView -> WindowManagerGlobal.addView -> ViewRootImpl.setView -> ViewRootImpl.requestLayoutViewRootImpl也是在 ActivityThread.handleResumeActivity 里创建的。

  • ViewRootImplDecorView 的关系

ViewRootImpl.setView 里,通过 DecorView.assignParentViewRootImpl 设置为 DecorView 的 parent。

三、触摸机制

  1. getActiongetActionMask区别

getAction获取到的是多点触控融合到一块的模糊信息,而getActionMask获取的是准确信息,支持多点触控。两者之间没有性能差别。

  1. 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只做了各种取消操作,这里不再单独分析。

  1. dispatchTouchEvent源码分析

ViewGroupdispatchTouchEvent

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"

;