Android view体系简析及自定义滑动ViewGroup的优化

本文主要简单介绍java层面的android view体系,了解view体系后,在此基础上讨论如何规范地实现自定义滑动ViewGroup,使得滑动达到流畅效果。之前由于不熟悉view体系,自定义实现的时间选择控件实现逻辑非常混乱,随意调用requestLayout,invalidate等方法,而且onlayout的实现混乱,导致的结果是滑动时view不断地进行重绘,导致卡顿,效果非常差。(本文仅简单分析原理过程,如希望深入理解,请参考源码或者相关大神的博客)

下面先简单介绍一下android view体系的相关知识,然后讨论优化时间选择控件(我们自己实现的滑动选取日期的控件)的方案。

view 体系

view体系是android源码里面开发者最常接触的模块。View体系主要包含activty,ActivityManager,ActivityManagerService,window,windowManager,WindowManagerService,ViewRoot,View Tree,SurfaceFlinger等模块。这里本文主要针对Activity,Window,WindowManager,ViewRoot以及如何生存管理View Tree做说明,其他内容我理解不多,就不说了。

view框架

UI布局以一颗树形结构存储在Activity端(View Tree),Window中存储了View Tree的根节点信息。通过WindowManager与后台服务进程WindowsManagerService以及SurfaceFlinger通信进行窗口管理以及UI渲染,实现UI显示。

Activity和Window以及View Tree

Activity用来显示应用程序的UI,每个Activity中包含一个名为mWindows的变量指向类型为PhoneWindow的窗口对象。我们开发过程中的窗口都是PhoneWindow对象。即:一个Activity对应一个Window。Activty负责管理UI的生命周期,而UI信息则保存在Window对象中。一个PhoneWindow的结构大概如下图-1所示:

图-1

一个Window除了自身的属性如大小,类型等,每个Window对象中有一个mDector变量指向DectorView。DectorView是每个Activity的View Tree的根节点,View Tree保存了该Activty上所有View的信息。Activity的View Tree中有title以及content。title即应用程序标题栏,content是开发者希望显示的内容,通过在Activity的onCreate方法中调用setContentView设置。

大多数时候,我们自定义ViewGroup都是在针对Activity上的View Tree进行一些列操作。下面让我们来详细了解一个View Tree的相关内容。首先先说一下View Tree是怎么一步一步被创建出来的。

View Tree的创建过程

1.调用startActivity触发启动Activity后,通过调用ActivityThread的handleLaunchActivity方法实现窗口(view tree)的创建。

private void handleLaunchActivity(ActivityClientRecord r,Intent customIntent) {

...

Activity a = performLaunchActivity(r,customIntent);

if(a !=null) {

r.createdConfig=newConfiguration(mConfiguration);

Bundle oldState = r.state;

handleResumeActivity(r.token, false,r.isForward,

!r.activity.mFinished && !r.startsNotResumed);

}

...

}

以上源码主要有两个过程:

1.performLaunchActivity过程,生成Activity以及并生成绑定相对应的Window窗口相关,并调用Activity的onCreate方法。

2.handleResumeActivity将生成的Window注册到WindowManagerService.

生成View Tree在第一个过程中实现。在onCreate方法中通过调用setContentView生成View Tree。OnCreate中调用的setContentView实际上调用的是PhoneWindow中的setContentView方法。

该方法的实现如下:

@Overridepublic void setContentView(int layoutResID) {

...

if(mContentParent==null) {

installDecor();//生成DectorView

}else if(!hasFeature(FEATURE_CONTENT_TRANSITIONS)) {

mContentParent.removeAllViews();

}

if(hasFeature(FEATURE_CONTENT_TRANSITIONS)) {

finalScene newScene = Scene.getSceneForLayout(mContentParent,layoutResID,

getContext());

transitionTo(newScene);

}else{

mLayoutInflater.inflate(layoutResID,mContentParent);

}

mContentParent.requestApplyInsets();

...

}

1.首先调用installDecor生成View Tree的根节点DectorView以及相应的标题栏。

2.然后用mLayoutInflater.inflate(layoutResID,mContentParent)生成xml里面写好的view 内容。最后把View Tree根节点DectorView赋值给Window的mDector完成View Tree的创建过程。

上文提过我们自定义ViewGroup其实很多时候都涉及到对View Tree的管理,很多操作都是在对View Tree进行操作,如requestLayout,invalidate等操作。理解系统对View Tree的管理工作有助于我们更高效的编写自定义ViewGroup,找出ViewGroup滑动卡顿的原因进行优化。

说完了View Tree的创建过程,下面谈谈View Tree的管理。

什么是ViewRoot

View Tree的管理工作主要由ViewRoot这个类进行,它的具体实现是ViewRootImpl.java,源码位于android.view。ViewRoot实例存在WindowManagerGlobal.java这个类中,用于管理View Tree以及在窗口管理过程中与远程WindowManagerService通信。

WindowManagerGlobal mRoots即viewRoot


ViewRoot实现的主要功能有:

ViewRoot工作方式

如上图所示:

■1.接收外部对View Tree的操作(外部输入事件,WMS回调消息),处理消息,进行事件分发。

■2.接收View Tree的内部事件,如requestLayout,invalidate,遍历View Tree,调整View Tree显示。

■3.负责Window与AMS WMS等远程服务通信。

下面介绍一下1和2,3的通信过程这里不介绍了,感兴趣的同学可以自己研究。

ViewRoot对View Tree的事件分发

外部事件主要有WindowsManagerService返回的回调消息以及用户对窗口的输入消息,首先都会传给ViewRoot,通过ViewRoot向View Tree分发事件,进入view的事件分发流程。如不了解事件分发流程,请阅读这篇文章 Android事件分发机制完全解析,带你从源码的角度彻底理解或者自行google.

ViewRoot对View Tree内部事件的处理

前面提到,View Tree自身可以触发的主要内部事件主要有requestLayout,invalidate。当然还有其他的一些内部事件,这里只分析这两个比较典型的事件。

requestLayout:请求重新布局,触发measure过程以及layout过程。

invalidate:请求重新绘制,触发draw过程,重新画view的内容。

内部事件从View Tree的叶子节点向上传递,传递同时设置响应的标志位,最后到达ViewRoot,触发ViewRoot安排一次针对View Tree的遍历,根据设置的标志位及变化,重新布局View Tree.

View Tree的遍历

ViewRoot对View Tree的遍历过程由performTraversals()函数实现,主要分为以下三个过程:

1.measure过程,测量view tree中所有view节点的大小。

2.layout过程,根据measure结果,将view tree中的view节点放到合适的位置。

3.draw过程,根据layout结果,在对应位置画出view节点的内容。

这三个过程在遍历过程中不是必须被执行的,根据情况可以跳过其中某个过程。内部事件发生以后,事件从下往上传递到ViewRoot请求一次遍历的过程中,会根据情况设置不同的标志位,这些标志位非常多。执行遍历的时候,根据不同标志位,跳过或者执行某个过程。

以上是简单介绍了这三个过程,如希望从源码角度深入了解这三个过程的具体实现,可以阅读这篇文章 Android应用程序窗口(Activity)的测量(Measure)、布局(Layout)和绘制(Draw)过程分析,或者自己阅读源码,源码在android.view.View.java。

重点

上文简单的介绍了android java层view体系,主要是针对View Tree的管理。之所以去看之前这方面的内容,主要是为了下面优化自定义ViewGroup做准备。起因是之前我们项目中有一个自定义的滑动日期选择控件,很多地方写得不够规范,滑动起来非常卡。下面介绍一下如何根据上文学到的知识自定义滑动ViewGroup。

我们在自定义ViewGroup时,一般需要重载以下方法,实现自己需要的效果:

■onmeasure//测量控件自身以及子view大小,在measure方法中会被调用

■onlayout//布局子view,在layout方法中会被调用

■dispatchTouchEvent//控制事件分发

■onInterceptTouchEvent//拦截事件

■onTouchEvent//根据事件,实现你想要的操作,滑动等。

这里容易犯的错误主要是requestLayout以及invalidate的滥用以及在onMeasure、onLayout中写了一些多余的操作。

1.上文分析,在没有必要的地方滥用requestLayout或者invalidate会导致ViewRoot触发一次对View Tree的遍历,如果view的层级很深,就会产生不必要的耗时操作,导致卡顿。

2.在onLayout或者onMeasure中进行一些不必要的操作,触发新的遍历过程,导致循环遍历。比如在onLayout中对子view调用setText函数,将会触发invalidate,进而触发一次新的遍历,结果导致循环遍历。不仅耗时,而且逻辑混乱,遇到什么问题很难定位。

建议:

自定义ViewGroup时一定要把针对事件的处理过程(外部事件,内部事件)和遍历过程分开,不要存在耦合。即:

■onmeasureonlayout的实现和ViewGroup对事件的处理没有任何关系,否则很有可能触发循环遍历,逻辑混乱。

总结:

如何优化?

遍历操作一般比较耗时,导致卡顿的主要原因是进行了不必要的遍历操作,应该尽量在滑动时尽可能少地触发遍历操作。

那么,根据上文分析,我们主要针对自定义日期选择控件做了如下优化:

1.去掉不必要的requestLayout、invalidate操作。

2.从onMeasure、onLayout中分离与测量,布局无关的操作,做到onMeasure之中只有计算大小的操作,onLayout中只有计算位置的操作。

3.针对事件(滑动等)的处理集中到onTouchEvent之中处理,与遍历过程无关。

4.一次事件最多只会触发一次遍历过程,比如一个move event只需要触发一次遍历过程就能得到想要的效果,减少遍历操作。

5.减少View Tree的层级。这个可以使用官方提供的Hierarchy Viewer工具优化视图层级。

难点:

我们项目自定义控件的子view是多个TextView,需求是TextView在滑动的同时需要根据滑动位置改变TextView的内容。

setText方法会触发view tree的一次遍历,快速滑动过程中如果不听的setText,不停地触发遍历,遍历操作是比较耗时的,这样就会导致滑动卡顿。

那么如何在快速滑动过程中改变textView的显示内容又不引发耗时的遍历操作呢?我们最终采用的方法是模仿android sdk中NumberPicker的处理方法,滑动过程中直接在画布上画出TextView的文本内容,这样就不会触发遍历了。事实证明这样确实达到了滑动流畅效果。

注:本人水平有限,仅是简单讨论,以上若有错及不足,欢迎指正!

如想进一步深入理解View体系,建议阅读参考资料中老罗博客及android源码。

参考资料:

android源码

深入理解Android内核设计思想---林学森

老罗博客Android应用程序窗口(Activity)实现框架简要介绍和学习计划

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 195,898评论 5 462
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 82,401评论 2 373
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 143,058评论 0 325
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,539评论 1 267
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,382评论 5 358
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,319评论 1 273
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,706评论 3 386
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,370评论 0 254
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,664评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,715评论 2 312
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,476评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,326评论 3 313
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,730评论 3 299
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,003评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,275评论 1 251
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,683评论 2 342
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,877评论 2 335

推荐阅读更多精彩内容