Android源码阅读 UI绘制流程
环境
- Java 11
- android 11
由于学习的课程api 不一致 导致源码有些关键方法无法进入仔细阅读 采用截图的方式理解思路
view添加到窗口
进入到源码中可以发现 ,每个activity 默认生成的代码中都会有一个setContentView方法,这个方法用于加载输入路径当前的布局以及后续的操作
底层本质还是调用一个委托去设置资源
@Override
public void setContentView(@LayoutRes int layoutResID) {
initViewTreeOwners();
getDelegate().setContentView(layoutResID);
}
在继续走进这个委托类的接口实现方法
@Override
public void setContentView(int resId) {
ensureSubDecor();
ViewGroup contentParent = mSubDecor.findViewById(android.R.id.content);
contentParent.removeAllViews();
LayoutInflater.from(mContext).inflate(resId, contentParent);
mAppCompatWindowCallback.getWrapped().onContentChanged();
}
ensureSubDecor()
进入这个decor 方法 下图为部分代码
if (!mSubDecorInstalled)
mSubDecor = createSubDecor();
createSubDecor()
根据实装值 来判断是否创建对应的decor
这个方法中有大量的系统资源整合 ,都是一些系统主题的装饰器放入到对应需要创建的window中,比如
- content
- themedContext
- subDecor 等等系统资源
再继续往下看 会看到一个赋值操作 将 mWindow.setContentView(subDecor);
赋值,
PS: WINDOW 对象的主要实现都在phone window 中 所以我们去phone window 寻找这个主要的方法
这个方法有两个主要实现 这里我们主要看 layouResId为参数的实现内容
@Override
public void setContentView(int layoutResID) {
// Note: FEATURE_CONTENT_TRANSITIONS may be set in the process of installing the window
// decor, when theme attributes and the like are crystalized. Do not check the feature
// before this happens.
// 先判断上下文父对象是否存在 是否创建内容装饰器(用于拿到需要的基础主题资源)
if (mContentParent == null) {
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 {
mLayoutInflater.inflate(layoutResID, mContentParent);
}
mContentParent.requestApplyInsets();
final Callback cb = getCallback();
if (cb != null && !isDestroyed()) {
cb.onContentChanged();
}
mContentParentExplicitlySet = true;
}
这里我们点进 generateLayout
方法,见名知意 这个方法主要是组装布局需要的主题属性和样式属性 这里看到
mDecor.onResourcesLoaded(mLayoutInflater, layoutResource);
加载资源的方法 根据不同的 feature 给layout赋不同的值,之后将布局信息添加到 mdecor
之后再 generateLayout() 返回一个 contentParent 也就是一个viewGroup
流程图
总结
- 在进入到activity 之后 会优先创建顶层布局 decorView 来设置系统需要的主题参数
- 将decorView 添加到基础布局中 返回 viewGroup 不同的主题加载不同的viewGroup 但是都会有一个容器OnDraw的id
- 最后被setContentView添加到布局中
View的绘制流程
根据上图 我们可以先去查看ActivityThread
找到 H这个类 他继承自 handle 内部实现了一个handleMessage
方法 内部会有一个调用链路
- handleMessage 调用 handleLaunchActivity 标识 启动一个选择的activity
- handleLaunchActivity 调用 handleResumeActivity
- handleResumeActivity 调用performResumeActivity最终在这个方法回调
需要注意的是在这个调用链路中 viewRootImpl performTraversal()
这个方法 ,绘制view的三个步骤就是在这个线程中完成的
测量
从最底层源码的测量可以看出 执行测量其实就是不断调用到最低层的 setMeasureDimensionRow 中对两个成员变量标记赋值,保存测量控件的宽高。
在view 绘制之前 安卓底层会先去利用 measure
测量对应view 的模式和尺寸,安卓给我们提供了一个叫做 measurespec类 是一个三十二位int的存储值,前两位代表模式,后三十位代表尺寸
- SpecMode(前两位) + SpecSize(后三十位)
核心方法
makeMeasureSpec
用于取值 mode 只需要取前两位 size 只需要取后三十位 组合成一个 MeasureSpec
同样 MeasureSpec 也有对应的getsize
和getmode
获取对应的值
getRootMeasureSpec
参数
- 子容器高度/宽度
- 顶级容器 高度/宽度
根据设置的布局属性 来设置对应的mode
帮助 performMeasure 是拿到需要的 measurespec
decor view 继承自 framelayout 查看他的 onMeasure 方法
通过递归的方式 判断并且设置不同规则父容器和子控件的测量方式
View 测量的对比图
完整测量自身流程
ps: 在自定义view 的时候 一定要重写 onmeasureDiemsion方法
在 view 设置测量自己之前会调用getdefaultsize 根据不同的模式获取到size 有趣的是 at 和 exactly 赋值都是父容器剩余的大小,所以在自定义view 需要重写一系列方法不然就可能出现问题
自定义容器和viwe 区别 :
- 自定义容器需要先测量子控件再量自己
- view 直接测量自己即可
布局
核心方法
view.setFrame()
对控件的上下左右赋值 设置完成 控件需要摆放的位置就确定了
view.onlayout()
如果此时是容器 则需要调用此方法 确定子控件的位置
绘制
核心方法
view.draw()
/*
* Draw traversal performs several drawing steps which must be executed
* in the appropriate order:
*
* 1. Draw the background
* 2. If necessary, save the canvas' layers to prepare for fading
* 3. Draw view's content
* 4. Draw children
* 5. If necessary, draw the fading edges and restore layers
* 6. Draw decorations (scrollbars for instance)
* 7. If necessary, draw the default focus highlight
*/
viewGroup.dispatchDraw()
总结
绘制过程
ViewGroup
- 绘制背景
- 绘制自己
- 绘制 子控件
- 绘制前景 滚动条等
View
- 绘制背景
- 调用自己的 ondraw
- 绘制前景 滚动条等