Textview源码+绘制过程解析
为什么要规定所有与UI相关的操作都必须在主线程中执行呢?我们知道,这些与UI相关的操作都涉及到大量的控件内部状态以及需要访问窗口的绘图表面,也就是说,要大量地访问控件类的成员变量以及窗口绘图表面里面的图形缓冲区,因此,如果不将这些与UI相关的操作限定在同一个线程中执行的话,那么就会涉及到线程同步问题。线程同步的开销是很大的,因此,就要保证那些与UI相关的操作都在同一个线程中执行。这个负责执行UI相关操作的线程便是应用程序进程的主线程,因此我们也将应用程序进程的主线程称为UI线程。
我们知道,应用程序进程的主线程除了负责执行与UI相关的操作之外,还负责响应用户的输入,因此,我们就要尽量地避免执行很耗时的UI操作,否则的话,系统就会由于应用程序进程的主线程无法及时响应用户输入而弹出ANR对话框。
那么,有没有办法让某一个控件的UI享有独立的图形缓冲区呢?也就是这个控件不将自己的UI数据填入到它的宿主窗口的绘图表面的图形缓冲区里面去。如果可以的话,那么我们就可以在另外一个独立的线程中绘制该控件的UI。这样做的好处是显而易见——可以在这个独立的线程执行相对比较耗时的UI绘制操作而不会导致主线程无法及时响应用户输入。答案是肯定的,在接下来的一篇文章中,我们就分析一个可以具有独立图形缓冲区的控件——SurfaceView。
Android应用程序窗口(Activity)的测量(Measure)、布局(Layout)和绘制(Draw)过程分析
1. Android应用程序窗口的测量过程
Step 1. View.measure
当View类的成员函数measure决定要重新测量当前视图的宽度和高度之后,它就会首先将成员变量mPrivateFlags的MEASURED_DIMENSION_SET位设置为0,接着再调用另外一个成员函数onMeasure来真正执行测量宽度和高度的操作。View类的成员函数onMeasure执行完成之后,需要再调用另外一个成员函数setMeasuredDimension来将测量好的宽度和高度设置到View类的成员变量mMeasuredWidth和mMeasuredHeight中,并且将成员变量mPrivateFlags的EASURED_DIMENSION_SET位设置为1。这个操作是强制的,因为当前视图最终就是通过View类的成员变量mMeasuredWidth和mMeasuredHeight来获得它的宽度和高度的。为了保证这个操作是强制的,View类的成员函数measure再接下来就会检查成员变量mPrivateFlags的EASURED_DIMENSION_SET位是否被设置为1了。如果不是的话,那么就会抛出一个类型为IllegalStateException的异常来。
View类的成员函数onMeasure一般是由其子类来重写的。例如,对于用来应用程序窗口的顶层视图的DecorView类来说,它是通过父类FrameLayout来重写祖父类View的成员函数onMeasure的。因此,接下来我们就分析FrameLayout类的成员函数onMeasure的实现。
Step 2. FrameLayout.onMeasure
FrameLayout类是从ViewGroup类继承下来的,后者用来描述一个视图容器,它有一个类型为View的数组mChildren,里面保存的就是它的各个子视图。ViewGroup类所供了两个成员函数getChildCount和getChildAt,它们分别用来获得一个视图容器所包含的子视图的个数,以及获得每一个子视图。
FrameLayout类的成员函数onMeasure首先是调用另一个成员函数measureChildWithMargins来测量每一个子视图的宽度和高度,并且找到这些子视图的最大宽度和高度值,保存在变量maxWidth和maxHeight 中。
FrameLayout类的成员函数onMeasure接着再将前面得到的宽度maxWidth和高度maxHeight分别加上当前视图所设置的Padding值,其中,(mPaddingLeft,mPaddingRight,mPaddingTop,mPaddingBottom )表示当前视图的内容区域的左右上下四条边分别到当前视图的左右上下四条边的距离,它们是父类View的四个成员变量,(mForegroundPaddingLeft,mForegroundPaddingRight,mForegroundPaddingTop,mForegroundPaddingBottom)表示当前视图的各个子视图所围成的区域的左右上下四条边到当前视视的前景区域的左右上下四条边的距离。从这里就可以看出,当前视图的内容区域的大小就等于前景区域的大小,而前景区域的大小大于等于各个子视图的所围成的区域,这是因为前景区域本来就是用来覆盖各个子视图所围成的区域的。
加上各个Padding值之后,得到的宽度maxWidth和高度maxHeight还不是最终的宽度和高度,还需要考虑以下两个因素:
1. 当前视图是否设置有最小宽度和高度。如果设置有的话,并且它们比前面计算得到的宽度maxWidth和高度maxHeight还要大,那么就将它们作为当前视图的宽度和高度值。
2. 当前视图是否设置有前景图。如果设置有的话,并且它们比前面计算得到的宽度maxWidth和高度maxHeight还要大,那么就将它们作为当前视图的宽度和高度值。
Step 3. ViewGroup.measureChildWithMargins
参数child用来描述当前要测量大小的子视图,参数parentWidthMeasureSpec和parentHeightMeasureSpec用来描述当前子视图可以获得的最大宽度和高度,参数widthUsed和heightUsed用来描述父窗口已经使用了的宽度和高度。ViewGroup类的成员函数measureChildWithMargins必须要综合考虑上述参数,以及当前正在测量的子视图child所设置的大小和Margin值,还有当前视图容器所设置的Padding值,来得到当前正在测量的子视图child的正确宽度childWidthMeasureSpec和高度childHeightMeasureSpec,这是通过调用ViewGroup类的另外一个成员函数getChildMeasureSpec来实现的。
得到了当前正在测量的子视图child的正确宽度childWidthMeasureSpec和高度childHeightMeasureSpec之后,就可以调用它的成员函数measure来设置它的大小了,即执行前面Step 1的操作。注意,如果当前正在测量的子视图child描述的也是一个视图容器,那么它又会重复执行Step 2和Step 3的操作,直到它的所有子孙视图的大小都测量完成为止。
2. Android应用程序窗口的布局过程
Step 1. View.layout
public class View implements Drawable.Callback, KeyEvent.Callback, AccessibilityEventSource {
...... int mPrivateFlags;
...... public final void layout(int l, int t, int r, int b) {
boolean changed = setFrame(l, t, r, b);
if (changed || (mPrivateFlags & LAYOUT_REQUIRED) == LAYOUT_REQUIRED) {
...... onLayout(changed, l, t, r, b);
mPrivateFlags &= ~LAYOUT_REQUIRED;
}
mPrivateFlags &= ~FORCE_LAYOUT;
} ......
}
参数l、t、r和b分别用来描述当前视图的左上右下四条边与其父视图的左上右下四条边的距离,这样当前视图通过这四个参数就可以知道它在父视图中的位置以及大小。
View类的成员函数layout首先调用另外一个成员函数setFrame来设置当前视图的位置以及大小。设置完成之后,如果当前视图的大小或者位置与上次相比发生了变化,那么View类的成员函数setFrame的返回值changed就会等于true。在这种情况下, View类的成员函数layout就会继续调用另外一个成员函数onLayout重新布局当前视图的子视图。此外,如果此时View类的成员变量mPrivateFlags的LAYOUT_REQUIRED位不等于0,那么也表示当前视图需要重新布局它的子视图,因此,这时候View类的成员函数layout也会调用另外一个成员函数onLayout。
当前视图的子视图都重新布局完成之后,View类的成员函数layout就可以将成员变量mPrivateFlags的LAYOUT_REQUIRED位设置为0了,因为此时当前视图及其子视图都已经执行了一次布局操作了。
View类的成员函数layout最后还会将成员变量mPrivateFlags的FORCE_LAYOUT位设置为0,也是因为此时当前视图及其子视图的布局已经是最新的了。
Step 2. View.setFrame
View类的成员变量mLeft、mRight、mTop和mBottom分别用来描述当前视图的左右上下四条边与其父视图的左右上下四条边的距离,如果它们的值与参数left、right、top和bottom的值不相等,那么就说明当前视图的大小或者位置发生变化了。这时候View类的成员函数setFrame就需要将参数left、right、top和bottom的值分别记录在成员变量mLeft、mRight、mTop和mBottom中。在记录之前,还会执行两个操作:
1. 将成员变量mPrivateFlags的DRAWN位记录在变量drawn中,并且调用另外一个成员函数invalidate来检查当前视图上次请求的UI绘制操作是否已经执行。如果已经执行了的话,那么就会再请求执行一个UI绘制操作,以便可以在修改当前视图的大小和位置之前,将当前视图在当前位置按照当前大小显示一次。在接下来的Step 3中,我们再详细分析View类的成员函数invalidate的实现。
2. 计算当前视图上一次的宽度oldWidth和oldHeight,以便接下来可以检查当前视图的大小是否发生了变化。
当前视图距离父视图的边距一旦设置好之后,它就是一个具有边界的视图了,因此,View类的成员函数setFrame接着还会将成员变量mPrivateFlags的HAS_BOUNDS设置为1。
View类的成员函数setFrame再接下来又会计算当前视图新的宽度newWidth和高度newHeight,如果它们与上一次的宽度oldWidth和oldHeight的值不相等,那么就说明当前视图的大小发生了变化,这时候就会调用另外一个成员函数onSizeChanged来让子类有机会处理这个变化事件。
View类的成员函数setFrame接下来继续判断当前视图是否是可见的,即成员变量mViewFlags的VISIBILITY_MASK位的值是否等于VISIBLE。如果是可见的话,那么就需要将成员变量mPrivateFlags的DRAWN位设置为1,以便接下来可以调用另外一个成员函数invalidate来成功地执行一次UI绘制操作,目的是为了将当前视图马上显示出来。
View类的成员变量mPrivateFlags的DRAWN位描述的是当前视图上一次请求的UI绘制操作是否已经执行过了。如果它的值等于1,就表示已经执行过了,否则的话,就表示还没在等待执行。前面第一次调用View类的成员函数invalidate来检查当前视图上次请求的UI绘制操作是否已经执行时,如果发现已经执行了,那么就会重新请求执行一次新的UI绘制操作,这时候会导致当前视图的成员变量mPrivateFlags的DRAWN位重置为0。注意,新请求执行的UI绘制只是为了在修改当前视图的大小以及大小之前,先将它在上一次设置的大小以及位置中绘制出来,这样就可以使得当前视图的大小以及位置出现平滑的变换。换句话说,新请求执行的UI绘制只是为了获得一个中间效果,它不应该影响当前视图的绘制状态,即不可以修改当前视图的成员变量mPrivateFlags的DRAWN位。因此,我们就需要在前面第一次调用View类的成员函数invalidate前,先将当前视图的成员变量mPrivateFlags的DRAWN位保存下来,即保存在变量drawn中,然后等到调用之后,再将变量drawn的值恢复到当前视图的成员变量mPrivateFlags的DRAWN位中去。
另一方面,如果当前视图的大小和位置发生了变化,View类的成员函数setFrame还会将成员变量mBackgroundSizeChanged的值设置为true,以便可以表示当前视图的背景大小发生了变化。
最后,View类的成员函数setFrame将变量changed的值返回给调用者,以便调用者可以知道当前视图的大小和位置是否发生了变化。
Step 3. View.invalidate
public class View implements Drawable.Callback, KeyEvent.Callback, AccessibilityEventSource {
...... protected ViewParent mParent;
...... int mPrivateFlags;
...... public void invalidate() {
...... if ((mPrivateFlags & (DRAWN | HAS_BOUNDS)) == (DRAWN | HAS_BOUNDS)) {
mPrivateFlags &= ~DRAWN & ~DRAWING_CACHE_VALID;
final ViewParent p = mParent;
final AttachInfo ai = mAttachInfo;
if (p != null && ai != null) {
final Rect r = ai.mTmpInvalRect;
r.set(0, 0, mRight - mLeft, mBottom - mTop);
// Don't call invalidate -- we don't want to internally scroll
// our own bounds
p.invalidateChild(this, r);
}
}
} ......
}
View类的成员函数invalidate首先检查成员变量mPrivateFlags的DRAWN位和HAS_BOUNDS位是否都被设置为1。如果是的话,那么就说明当前视图上一次请求执行的UI绘制操作已经执行完成了,这时候View类的成员函数invalidate才可以请求执行新的UI绘制操作。
View类的成员函数invalidate在请求新的UI绘制操作之前,会将成员变量mPrivateFlags的DRAWN位和DRAWING_CACHE_VALID位重置为0,其中,后者表示当前视图正在缓存的一些绘图对象已经失效了,这是因为接下来就要重新开始绘制当前视图的UI了。
请求绘制当前视图的UI是通过调用View类的成员变量mParent所描述的一个ViewParent接口的成员函数invalidateChild来实现的。前面我们假设当前视图是应用程序窗口的顶层视图,即它是一个类型为DecoreView的视图,它的成员变量mParent指向的是与其所关联的一个ViewRoot对象。因此,绘制当前视图的UI的操作实际上是通过调用ViewRoot类的成员函数invalidateChild来实现的。
注意,在调用ViewRoot类的成员函数invalidateChild的成员函数invalidateChild来绘制当前视图的UI之前,会将当前视图即将要绘制的区域记录在View类的成员变量mAttachInfo所描述的一个AttachInfo对象的成员变量mTmpInvalRect中。
Step 4. ViewRoot.invalidateChild
ViewRoot类的成员函数invalidateChild首先调用另外一个成员函数checkThread来检查当前正在执行的是否是一个UI线程。如果不是的话,ViewRoot类的成员函数checkThread就会抛出一个异常出来。这是因为所有的UI操作都必须要在UI线程中执行。
ViewRoot类的成员函数invalidateChild接下来还会检查当前正在处理的应用程序窗口在Y轴上是否出现有滚动条,即成员变量mCurScrollY的值不等于0, 或者前正在处理的应用程序窗口是否运行在兼容模式之下,即成员变量mTranslator的值不等于null。当一个应用程序窗口运行在兼容模式时,它显示出来的大小和它实际被设置的大小是不一样的,要经过相应的转换处理。对于上述这两种情况,ViewRoot类的成员函数invalidateChild都需要调整参数dirty所描述的一个需要重新绘制的区域的大小和位置。
调整好参数dirty所描述的一个需要重新绘制的区域之后, ViewRoot类的成员函数invalidateChild就将它所描述的一个区域与成员变量mDirty所描述的一区域执行一个合并操作,并且将得到的新区域保存在成员变量mDirty中。从这个操作就可以看出,ViewRoot类的成员变量mDirty描述的就是当前正在处理的应用程序窗口下一次所要重新绘制的总区域。
设置好当前正在处理的应用程序窗口下一次所要重新绘制的总区域之后,ViewRoot类的成员函数invalidateChild最后就检查成员变量mWillDrawSoon的值是否不等于true。如果ViewRoot类的成员mWillDrawSoon的值等于true的话,那么就说明UI线程的消息队列中已经有一个DO_TRAVERSAL消息在等待执行了,这时候就不需要调用ViewRoot类的成员函数scheduleTraversals来向UI线程的消息队列发送一个DO_TRAVERSAL消息了,否则的话,就需要调用ViewRoot类的成员函数scheduleTraversals来向UI线程的消息队列发送一个DO_TRAVERSAL消息。
ViewRoot类的成员函数scheduleTraversals在前面Android应用程序窗口(Activity)的绘图表面(Surface)的创建过程分析一文中已经分析过了,这里不再详述。
这一步执行完成之后,返回到前面的Step 1中,即View类的成员函数layout中,接下来它就会调用另外一个成员函数onLayout来重新布局当前视图的子视图的布局了。View类的成员函数onLayout是由子类来重写的,并且只有当该子类描述的是一个容器视图时,它才会重写父类View的成员函数onLayout。前面我们已经假设当前正在处理的是应用程序窗口的顶层视图,它的类型为DecorView,并且它描述的是一个容器视图,因此,接下来我们就会继续分析DecorView类的成员函数onLayout的实现。
事实上,DecorView类是通过FrameLayout类来间接继承View类的,并且它的成员函数onLayout是从FrameLayout类继承下来的,因此,接下来我们实际上要分析的是FrameLayout类的成员函数onLayout的实现。
Step 5. FrameLayout.onLayout
FrameLayout类的成员变量mPaddingLeft、mPaddingRight、mPaddingTop、mPaddingBottom和mForegroundPaddingLeft、mForegroundPaddingRight、mForegroundPaddingTop、mForegroundPaddingBottom的含义我们在前面分析Android应用程序窗品的测量过程时已经解释过了,它们描述的是当前视图的内边距,而参数left、top、right和bottom描述的是当前视图的外边距,即它与父窗口的边距。通过上述这些参数,我们就可以得到当前视图的子视图所能布局在的区域。
FrameLayout类的成员函数onLayout通过一个for循环来布局当前视图的每一个子视图。如果一个子视图child是可见的,那么FrameLayout类的成员函数onLayout就会根据当前视图可以用来显示子视图的区域以及它所设置的gravity属性来得到它在应用程序窗口中的左上角位置(childeLeft,childTop)。
当一个子视图child在应用程序窗口中的左上角位置确定了之后,再结合它在前面的测量过程中所确定的宽度width和高度height,我们就可以完全地确定它在应用程序窗口中的布局了,即可以调用它的成员函数layout来设置它的位置和大小了,这刚好就是前面的Step 1所执行的操作。注意,如果当前正在布局的子视图child描述的也是一个视图容器,那么它又会重复执行Step 5的操作,直到它的所有子孙视图都布局完成为止。
3. Android应用程序窗口的绘制过程
ViewRoot类的成员函数draw首先会创建一块画布,接着再在画布上绘制Android应用程序窗口的UI,最后再将画布的内容交给SurfaceFlinger服务来渲染,这个过程如图4所示:
Step 8. View.draw
这个函数定义在文件frameworks/base/core/java/android/view/View.java中,它主要是完成以下六个操作:
1. 绘制当前视图的背景。
2. 保存当前画布的堆栈状态,并且在在当前画布上创建额外的图层,以便接下来可以用来绘制当前视图在滑动时的边框渐变效果。
3. 绘制当前视图的内容。
4. 绘制当前视图的子视图的内容。
5. 绘制当前视图在滑动时的边框渐变效果。
6. 绘制当前视图的滚动条。
在上面六个操作中,有些是可以优化的。例如,如果当前视图的某一个子视图是不透明的,并且覆盖了当前视图的内容,那么当前视图的背景以及内容就不会绘制了,即不用执行第1和第3个操作。又如,如果当前视图不是处于滑动的状态,那么第2和第5个操作也是不用执行的。
Textview源码+绘制过程解析的更多相关文章
- MyBatis 源码分析 - 配置文件解析过程
* 本文速览 由于本篇文章篇幅比较大,所以这里拿出一节对本文进行快速概括.本篇文章对 MyBatis 配置文件中常用配置的解析过程进行了较为详细的介绍和分析,包括但不限于settings,typeAl ...
- 笔记-twisted源码-import reactor解析
笔记-twisted源码-import reactor解析 1. twisted源码解析-1 twisted reactor实现原理: 第一步: from twisted.internet ...
- 鸿蒙内核源码分析(ELF解析篇) | 你要忘了她姐俩你就不是银 | 百篇博客分析OpenHarmony源码 | v53.02
百篇博客系列篇.本篇为: v53.xx 鸿蒙内核源码分析(ELF解析篇) | 你要忘了她姐俩你就不是银 | 51.c.h.o 加载运行相关篇为: v51.xx 鸿蒙内核源码分析(ELF格式篇) | 应 ...
- v72.01 鸿蒙内核源码分析(Shell解析) | 应用窥伺内核的窗口 | 百篇博客分析OpenHarmony源码
子曰:"苟正其身矣,于从政乎何有?不能正其身,如正人何?" <论语>:子路篇 百篇博客系列篇.本篇为: v72.xx 鸿蒙内核源码分析(Shell解析篇) | 应用窥视 ...
- Ubuntu 14.04 LTS 下 android 2.3.5 源码编译过程
Ubuntu 14.04 LTS 下 android 2.3.5 源码编译过程 在新的Ubuntu 64位系统下去编译早期的安卓源码是会出现很多问题的,因为64位系统在安装完成后,很多32位的兼容 ...
- mysql-5.5.28源码安装过程中错误总结
介绍一下关于mysql-5.5.28源码安装过程中几大错误总结,希望此文章对各位同学有所帮助.系统centOS 6.3 mini (没有任何编译环境)预编译环境首先装了众所周知的 cmake(yum ...
- 英蓓特Mars board的android4.0.3源码编译过程
英蓓特Mars board的android4.0.3源码编译过程 作者:StephenZhu(大桥++) 2013年8月22日 若要转载,请注明出处 一.编译环境搭建及要点: 1. 虚拟机软件virt ...
- InfluxDB源码目录结构解析
操作系统 : CentOS7.3.1611_x64 go语言版本:1.8.3 linux/amd64 InfluxDB版本:1.1.0 influxdata主目录结构 [root@localhost ...
- Spring源码入门——DefaultBeanNameGenerator解析 转发 https://www.cnblogs.com/jason0529/p/5272265.html
Spring源码入门——DefaultBeanNameGenerator解析 我们知道在spring中每个bean都要有一个id或者name标示每个唯一的bean,在xml中定义一个bean可以指 ...
随机推荐
- 利用App漏洞获利2800多万元,企业该如何避免类似事件?
上个月,上海警方抓捕了一个利用网上银行漏洞非法获利的犯罪团伙,该团伙利用银行App漏洞非法获利2800多万元. 据悉,该团伙使用技术软件成倍放大定期存单金额,从而非法获利.理财邦的一篇文章分析了犯罪嫌 ...
- Loop List
Loop List is very common in interview. This article we give a more strict short statement about its ...
- Swift5 语言参考(八) 模式
模式表示单个值或复合值的结构.例如,元组的结构是两个元素的逗号分隔列表.因为模式表示值的结构而不是任何一个特定值,所以可以将它们与各种值匹配.例如,模式匹配元组和任何其他两元素元组.除了将模式与值匹配 ...
- MyBatis全局配置文件标签详解
一.全局配置文件结构 configuration 配置 properties 属性:可以加载properties配置文件的信息 settings 设置:可以设置mybatis的全局属性 typeAli ...
- odoo开发笔记 -- odoo和postgresql数据库导入相关
odoo数据库 导入.导出 首先odoo框架下postgresql数据库中,表结构的存储方式: 存在id(小写),并没有所谓的外部ID 例如数据库中的国家表:模块名_tb_country (注意: ...
- C语言编码转换gb2312 to utf8,utf8 to gb2312 代码,GCC编译,支持Windows、Linux
编译:gcc -o f.exe f.c -liconv #include <stdio.h> #include <stdlib.h> #include <stddef.h ...
- CentOS7.x编译安装nginx,实现HTTP2
网站使用HTTP2有助于网站加速及更安全,要配置HTTP2必须满足两个条件:①openssl的版本必须在1.0.2e及以上.②nginx的版本必须在1.9.5以上 一.准备工作 配置HTTP2之前需 ...
- 匿名类、包、权限修饰符_DAY10
1:内部类(理解) (1)把类定义在一个类的内部. (2)特点: A:内部类可以直接使用外部类的成员,包括私有. B:外部类要使用内部类成员,必须创建对象使用. 例子: public c ...
- SkipList 跳跃表
引子 考虑一个有序表:14->->34->->50->->66->72 从该有序表中搜索元素 < 23, 43, 59 > ,需要比较的次数分别为 ...
- 测试JavaScript数组Array
<script> var numbers = [1, 2, 3, 4, 5]; function isLessThan3(value,index,array) { var returnVa ...