Android进阶 布局绘制流程 一 setContentView源码解读
本文承接上文 view 绘制流程 一。上文跟随源码分析了 setContentVIew() 后的部分 关于 PhoneWindow 和 Layoutinfate 部分的源码解析。该篇文章本人将继续学习和总结 ,并简单提及到经典面试题。本文大概阅读时间:30分钟 Andorid SDK :28.0 。
上文 分析道 PhoneWindow 的 setContentView 进行了分部解读
@Override
public void setContentView(int layoutResID) {
if (mContentParent == null) {
// view绘制流程 一 第一节
installDecor();
} else if (!hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
mContentParent.removeAllViews();
}
if (hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
final Scene newScene = Scene.getSceneForLayout(mContentParent, layoutResID,
getContext());
transitionTo(newScene);
} else {
// view 绘制流程 一 第四节
mLayoutInflater.inflate(layoutResID, mContentParent);
}
// 该篇我们顺序从该方法继续研究学习。!!!!!!
mContentParent.requestApplyInsets();
final Callback cb = getCallback();
if (cb != null && !isDestroyed()) {
cb.onContentChanged();
}
mContentParentExplicitlySet = true;
}
我们正式开始!
ViewRootImpl
// view 类
public void requestApplyInsets() {
requestFitSystemWindows();
}
public void requestFitSystemWindows() {
if (mParent != null) {
mParent.requestFitSystemWindows();
}
}
这里的mParent是ViewParent的具体实现ViewRootImpl,所以调用的是ViewRootImpl里的requestFitSystemWindows方法。
// ViewRootImpl 类
@Override
public void requestFitSystemWindows() {
// 检查线程 是否是在主线程
checkThread();
mApplyInsetsRequested = true;
// 视图绘制
scheduleTraversals();
}
ViewRootImpl.scheduleTraversals()
void scheduleTraversals() {
// 没有正在绘制的 任务才能进入
if (!mTraversalScheduled) {
mTraversalScheduled = true;
mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier();
// 发送一个消息
mChoreographer.postCallback(
Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
if (!mUnbufferedInputDispatch) {
scheduleConsumeBatchedInput();
}
...
}
}
// Runnable实现
final class TraversalRunnable implements Runnable {
@Override
public void run() {
// 绘制方法
doTraversal();
}
}
void doTraversal() {
if (mTraversalScheduled) {
mTraversalScheduled = false;
mHandler.getLooper().getQueue().removeSyncBarrier(mTraversalBarrier);
if (mProfile) {
Debug.startMethodTracing("ViewAncestor");
}
// 最重要的地方 执行view 的绘制流程
performTraversals();
if (mProfile) {
Debug.stopMethodTracing();
mProfile = false;
}
}
}
performTraversals()
private void performTraversals() {
...
// 第一次加载View,需要调整窗口大小,需要适应系统窗口,视图显示状态改变,
// 视图布局参数不为空,强制窗口重新布局。才可能执行测量
if (mFirst || windowShouldResize || insetsChanged ||
viewVisibilityChanged || params != null || mForceNextWindowRelayout) {
...
// 窗口没有停止,或者通知需要绘制
if (!mStopped || mReportNextDraw) {
...
if (focusChangedDueToTouchMode || mWidth != host.getMeasuredWidth()
|| mHeight != host.getMeasuredHeight() || contentInsetsChanged ||
updatedConfiguration) {
...
// 1.第一步:测量
// Ask host how big it wants to be
performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
...
}
}
} else {
...
}
...
if (didLayout) {// 执行布局
// 2.第二步:布局
performLayout(lp, mWidth, mHeight);
...
}
...
// 如果没有取消绘制,并且不是新的Surface,那么执行绘制
if (!cancelDraw && !newSurface) {
...
// 3.第三步:绘制
performDraw();
} else {// 如果取消了绘制或者是新的Surface,那么要重新测量、布局和绘制
...
}
mIsInTraversal = false;
}
重要的方法就是在上述的代码中 通过 performMeasure(), performLayout() ,performDraw() 三个方法,方法内也分别调用了 View的 measure layout 和 draw 方法。这也印证了我们 在编写自定义View的时候 需要默认实现的三个方法的由来,也说明了三个方法的顺序。
View 绘制相关面试题
事件分发
前边的文章已经提到过,这里就放两张图片,一图胜千言
触摸事件分发 流程图参考文章
Dialog 和 Activity 点击事件问题
案例:当Dialog 现实的时候点击 Activity 部分按钮是没有反映的,为什么呢?
我们先看下 Dialog的初始化源码
Dialog(@NonNull Context context, @StyleRes int themeResId, boolean createContextThemeWrapper) {
if (createContextThemeWrapper) {
if (themeResId == ResourceId.ID_NULL) {
final TypedValue outValue = new TypedValue();
context.getTheme().resolveAttribute(R.attr.dialogTheme, outValue, true);
themeResId = outValue.resourceId;
}
mContext = new ContextThemeWrapper(context, themeResId);
} else {
mContext = context;
}
mWindowManager = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
//新建PhoneWindow
final Window w = new PhoneWindow(mContext);
mWindow = w;
//将回调注入到window当中
w.setCallback(this);
w.setOnWindowDismissedCallback(this);
w.setOnWindowSwipeDismissedCallback(() -> {
if (mCancelable) {
cancel();
}
});
w.setWindowManager(mWindowManager, null, null);
w.setGravity(Gravity.CENTER);
mListenersHandler = new ListenersHandler(this);
}
这里创建好 phoneWindow 后setContentView() 设置布局,这里就绕到了 上篇文章的 源码顺序中,通过 installDecor() ,创建 DecorView 将我们传入的布局id 带入,通过 xml pull 方式迭代读取 view 最后返回一个view 对象,并将其添加到 DecorView中。
public void show() {
...
mWindowManager.addView(mDecor, l);
...
}
public void addView(View view, ViewGroup.LayoutParams params,
Display display, Window parentWindow) {
...
//创建ViewRootImpl
root = new ViewRootImpl(view.getContext(), display);
view.setLayoutParams(wparams);
mViews.add(view);
mRoots.add(root);
mParams.add(wparams);
try {
// setView
root.setView(view, wparams, panelParentView);
} catch (RuntimeException e) {
...
}
}
}
Dialog 通过show() 的方式,将DecorView 传给 WindowManager实现类 。 经过底层会调用到ViewRootImpl的ViewPostImeInputStage里面来。但是因为ViewRootImpl存在着多个,InputManagerService 会根据Z轴的高度,获取最近一个窗口,然后执行对应ViewRootImpl里面ViewPostImeInputStage的监听方法->执行mView.dispatchPointerEvent(event)。所以点击事件便在最上层 的Dialog中消费了。
绘制卡顿源码追踪
由于这里 源码层级更多,不是一篇两篇能够说清楚的,推荐一个大佬的 博文