view是怎么被展示在手机上的?
我们先了解下window、windowManager等相关知识:
在activity的attach方法里(我不管谁调用了,只知道这个是开始就会做的事情),我们可以看到activity有一个window(PhoneWindow)成员变量,在这里初始化的。然后接着又给这个window设置了WindowManager(其实就是contextImpl中static块中初始化好的windowManger,系统级的,只有一个),这个windowManage又是什么呢,他是个接口,继承了ViewManager接口(它定义了addView,updateVIewlayout,removeView三个方法)。他的实现类其实就是一个WindowManagerImpl,就是说给windowManager塞了一个WindowManagerImpl的实例,这个WindowManagerImpl里有个单例成员,是windowManagerGlobal,它里面有一些list,存放了view,viewRootImpl,windowmanager.layoutparams,它保存了当前应用程序添加的所有的View对象,已经相对应的ViewRootImpl对象和添加时使用的WindowManager.LayoutParams属性对象。
1.1 PhoneWindow
PhoneWindow是Android中的最基本的窗口系统,每个Activity 均会创建一个PhoneWindow对象,是Activity和整个View系统交互的接口。
1.2 DecorView
DecorView是当前Activity所有View的祖先,它并不会向用户呈现任何东西,它主要有如下几个功能,可能不全:
A. Dispatch ViewRoot分发来的key、touch、trackball等外部事件;
B. DecorView有一个直接的子View,我们称之为System Layout,这个View是从系统的Layout.xml中解析出的,它包含当前UI的风格,如是否带title、是否带process bar等。可以称这些属性为Window decorations。
C. 作为PhoneWindow与ViewRoot之间的桥梁,ViewRoot通过DecorView设置窗口属性。
1.3 System Layout
目前android根据用户需求预设了几种UI 风格,通过PhoneWindow通过解析预置的layout.xml来获得包含有不同Window decorations的layout,我们称之为System Layout,我们将这个System Layout添加到DecorView中,目前android提供了8种System Layout,如下图。
预设风格可以通过PhoneWindow方法requestFeature()来设置,需要注意的是这个方法需要在setContentView()方法调用之前调用。
1.4 Content Parent
Content Parent这个ViewGroup对象才是真真正正的ContentView的parent,我们的ContentView终于找到了寄主,它其实对应的是System Layout中的id为”content”的一个FrameLayout。这个FrameLayout对象包括的才是我们的Activity的layout(每个System Layout都会有这么一个id为”content”的一个FrameLayout)。
我们先看看activity的onCreate里的setContentView方法,在这个方法里:
public void setContentView(View view) {
getWindow().setContentView(view); //这里先获得phoneWindow
initWindowDecorActionBar();
}
在phoneWindow的setContentView里:
好。我们开始看setContentView里面一个重要的部分就是installDecor();
这个类创建了DecorView,这是一个什么东西呢?其实自身就是一个frameLayout,就是如果我们看activity层级结构的时候
可以看到最上层是个decorView,然后是一个linearlayout(顺带说一下:
会根据feattures得到窗口修饰文件布局,这个其实就是图上的linearLayout,并add到decorView中
还记得我们平时写应用Activity时设置的theme或者feature吗(全屏啥的,NoTitle等)?我们一般是不是通过XML的android:theme属性或者java的requestFeature()方法来设置的呢?譬如:
通过java文件设置: requestWindowFeature(Window.FEATURE_NO_TITLE); 通过xml文件设置: android:theme="@android:style/Theme.NoTitleBar"
对的,其实我们平时requestWindowFeature()设置的值就是在这里通过getLocalFeature()获取的;而android:theme属性也是通过这里的getWindowStyle()获取的。
),Linearlayout下面有一个id为content的frameLayout,和一个action_mode_bar_stub的ViewStub(性能优化之ViewStub,通过inflate后展示)。
接着再看,setContentView里执行了LayoutInflater的inflate方法,参数是我们传入的布局id,和上面获得的id为content的framelayout(所以,merge其中的一个用法就找到了所用之处,如果我们的根布局是个framelayout或者只有一个元素,就一个使用这个标签减少层级,至于我们为什么要减少层级,继续往下看),如此我们就知道上面的布局图为什么是那样了。//从此开始了将xml展示到屏幕中的长征
public View inflate(XmlPullParser parser, ViewGroup root, boolean attachToRoot) //这个方法有三个参数
//看第一个我们关心的重要的地方:这里先判断了如果我们的xml布局的根标签是<merge>的,那么就直接执行rInflate()否者按照另外一种方式处理,这种处理方式最终就是放到了rootView中,使用的是addView方法,所以我们在listView的getView中不能使用这样的方式
if (TAG_MERGE.equals(name)) {
if (root == null || !attachToRoot) {
throw new InflateException("<merge /> can be used only with a valid "
+ "ViewGroup root and attachToRoot=true");
}
rInflate(parser, root, attrs, false, false);
} else {
// Temp is the root view that was found in the xml
final View temp = createViewFromTag(root, name, attrs, false);
......
// Create layout params that match root, if supplied
params = root.generateLayoutParams(attrs);
if (!attachToRoot) {// 如果root不是null并且这个方法的第三个参数是false,就会把root的layoutParams赋给temp
// Set the layout params for temp if we are not
// attaching. (If we are, we use addView, below)
temp.setLayoutParams(params);
}
......
rInflate(parser, temp, attrs, true, true);//这个方法是做递归解析xml while()循环,添加到这个temp中去,利用addView。这里就是为什么我们要减少层级的一个原因,层级越多,就越耗时间循环啊!!!!!!教导了我们,尽可能使用相对布局
if (DEBUG) {
System.out.println("-----> done inflating children");
}
// We are supposed to attach all the views we found (int temp)
// to root. Do that now.
if (root != null && attachToRoot) {//如果第三个参数是true.则会把temp给add到root里去
root.addView(temp, params);
}
// Decide whether to return the root that was passed in or the
// top view found in xml.
if (root == null || !attachToRoot) {//如果没有null或者第三个参数是false,则返回temp(这里的如果root不是Null,则temp已经有layoutParams了)
result = temp;
}
从上面这段就能看出来,当我们使用listView的时候,在getView中 inflate(R.layout.xxxxx,null)的时候,直接返回了layout的根view,它的layoutParam为null,所以它的跟布局中的宽高会没有效果
所以总结下,就是如果调用的inflate(R.layout.xxxxx,null),只是返回了xml的根布局view,inflate(R.layout_xxxxxxx,rootview),这个在listView中会报错,rootView是listView,然而listView不支持addView,
在其他地方就是往rootView中addview(view)了,会直接展示出来。
inflate(R.layout_xxxxx,rootView,false),返回xml布局根布局view,并且拥有layoutPrarm,根宽高参数有效。
setContentView最后一个回调
if (cb != null && !isDestroyed()) {
cb.onContentChanged();
}
到这里setContentView结束了,view都准备好啦~那么我们这个时候显示到了屏幕上么?貌似木有吧,那又是怎么显示的?
这里我们又需要学习下activityThread了,在这个类里,会执行一段handleResumeActivity方法,在这个方法里,有一句r.activity.makeVisible();
void makeVisible() {
if (!mWindowAdded) {
ViewManager wm = getWindowManager();//这里的vm到底是什么,viewManager是个接口,实际上我们获得的这个vm就是之前在attach里面得到的一个WindowManagerImpl实例
wm.addView(mDecor, getWindow().getAttributes());//再看看WindowManagerImpl的addView方法其实就是保存了view,viewRootImpl,params,最终还有句话!!!!root.setView(view, wparams, panelParentView);
mWindowAdded = true;
}
mDecor.setVisibility(View.VISIBLE);
}
以上代码我们可以看到把mDecorView给set到ViewRootImpl中,这个setView方法中有句话:view.assignParent(this);他把DecorView的parent给置成了这个ViewRootImpl;
最终mDecor被设置为Visible;
说接下来的内容前,我们普及点知识:MeasureSpec,这里面全是static方法,他主要用来操作转换size、mode、测量结果,这里我们可以知道他主要是通过一个32位的二进制来计算的,最高2位表示的mode
mode有三大类,UNSPECIFEID(不限制大小,view的宽高设置0或者没有设置的时候),EXACITLY(精确的,view设置的match_parent),AT_MOST(根据自己尺寸有多大就是多大,view设置的wrap_parent),剩下的30位用来保存size
利用二进制的按位与和按位或来判断与取值
接着看,在之前我们已经看到了在ViewRootImpl中调用setView,然后在这个方法里,调用了requestLayout方法,然后执行了一个runnable,最终我们能看到一句话
// Ask host how big it wants to be
performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);
开始计算宽高啦,这个方法里面调用了mView.measure方法(这个方法是final的),这个mView其实就是我们之前setView传过来的根布局decorView!,在measure里面会调用onMeasure方法,我们都是通过重写这个方法重新设置宽高的这里必须要注意了,decorView调用measure方法,然后在这个方法内部调用了onMeasure方法,这个方法不是view的方法,而是decorView的父类frameLayout的onMeasure方法,记住这个就好办了,不然都不能理解怎么一层一层计算子view的大小的呢!!!!!
view的onMeasure方法里只调用一个方法
setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),
getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));
继续往里面看,一个view的最小宽高其实是由他的backgroud和他的minSize属性来决定的。然后当父类要求的测量模式是UNSPECIFIED的时候,那么就用这个view的大小,否则都使用的是父类规格的大小,一般来说这个onMeasure方法由系统实现好了
我们直接调用就可以了。
我们看一下ScrollView的onMeasure方法
if (getChildCount() > 0) {
final View child = getChildAt(0);//只看第一个child,为什么只看第一个呢?因为scrollView里面只能有一层.....
......
}
再看看ListView,如果我们在ListView外面套一层ScrollView,那么将会只能显示一行数据.....这是为什么呢?一句话,看看ListView源码吧
if (mItemCount > 0 && (widthMode == MeasureSpec.UNSPECIFIED ||
heightMode == MeasureSpec.UNSPECIFIED)) {
final View child = obtainView(0, mIsScrap);
measureScrapChild(child, 0, widthMeasureSpec);
childWidth = child.getMeasuredWidth();
childHeight = child.getMeasuredHeight();
childState = combineMeasuredStates(childState, child.getMeasuredState());
if (recycleOnMeasure() && mRecycler.shouldRecycleViewType(
((LayoutParams) child.getLayoutParams()).viewType)) {
mRecycler.addScrapView(child, 0);
}
}
if (widthMode == MeasureSpec.UNSPECIFIED) {
widthSize = mListPadding.left + mListPadding.right + childWidth +
getVerticalScrollbarWidth();
} else {
widthSize |= (childState&MEASURED_STATE_MASK);
}
if (heightMode == MeasureSpec.UNSPECIFIED) {
heightSize = mListPadding.top + mListPadding.bottom + childHeight +
getVerticalFadingEdgeLength() * 2;
}
if (heightMode == MeasureSpec.AT_MOST) {
// TODO: after first layout we should maybe start at the first visible position, not 0
heightSize = measureHeightOfChildren(widthMeasureSpec, 0, NO_POSITION, heightSize, -1);
}
setMeasuredDimension(widthSize , heightSize);
可以看到如果父类指定的是UNSPECIFIED,那么就会先计算listView第一个view的高度,并在之后赋给listView自己作为高度。 ListView、ScrollView都不限制子视图的高度,可以超过它本身。
所以我们曾经使用一个方法,重写了ListView的onMeasure方法
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int mExpandSpec = MeasureSpec.makeMeasureSpec(Integer.MAX_VALUE >> 2, //后30位是大小,现在就是00111111............
MeasureSpec.AT_MOST);
super.onMeasure(widthMeasureSpec, mExpandSpec);
}
这句话的意思就是,我自己指定用AT_MOST来替换掉了ScrollView的UNSPECIFIED,并且高度是最大。那么再看看源码,这下它会用
heightSize = measureHeightOfChildren(widthMeasureSpec, 0, NO_POSITION, heightSize, -1);来计算新的高度,
endPosition = (endPosition == NO_POSITION) ? adapter.getCount() - 1 : endPosition;
final AbsListView.RecycleBin recycleBin = mRecycler;
final boolean recyle = recycleOnMeasure();
final boolean[] isScrap = mIsScrap;
for (i = startPosition; i <= endPosition; ++i) {
child = obtainView(i, isScrap);
measureScrapChild(child, i, widthMeasureSpec);
.....
// Recycle the view before we possibly return from the method
if (recyle && recycleBin.shouldRecycleViewType(//这里计算高度的时候也会使用到回收栈
((LayoutParams) child.getLayoutParams()).viewType)) {
recycleBin.addScrapView(child, -1);
}
接着会遍历所有的child,计算最终高度。所以我们就可以展示了,但是却丢失了listView利用回收栈的特性。(因为我们给他传的高度是Integer.MAX_VALUE >> 2,最大,所以当在上面计算高度的时候发现够放下所有的item,可以通过断点调试看到这种情况下.mRecycler下面的mActiveView是ListView的总个数)
到这里,我们知道真正计算view高度的是在onMeasure中,那么这个方法的2个测量规格参数是怎么来的呢,是他们的父类ViewGroup的getChildMeasureSpec这个方法(以LinearLayout为例,入口onMeasure->measureVertical->measureChildBeforeLayout->measureChildWithMargins->getChildMeasureSpec).在这个方法里面我们可以总结出一个规律
parent | child | childMeasure |
EXACTLY | EXACTLY | EXACTLY |
EXACTLY | MATCH_PARENT | EXACTLY |
EXACTLY | WRAP_CONTENT | AT_MOST |
AT_MOST | EXACTLY | EXACTLY |
AT_MOST | MATCH_PARENT | AT_MOST |
AT_MOST | WRAP_CONTENT | AT_MOST |
UNSPECIFIED | EXACTLY | EXACTLY |
UNSPECIFIED | MATCH_PARENT | UNSPECIFIED |
UNSPECIFIED | WRAP_CONTENT | UNSPECIFIED |
为什么要说上面这个东西呢,我们再来看看ListView的onMeasure方法里面,会发现,当UNSPECIFIED显示会有问题,当AT_MOST的时候,会自己循环计算child可以放多少个,算出高度。只有当EXACTLY的时候,这些统统跳过,直接高度计算完毕。我记得我以前看过,listView的高度能设置match就不要设置wrap,不然adapter里面getView会执行很多次,一直不知道为什么,现在知道了吧!!!!!!所以我们在使用listView的时候,第一不要嵌套在scrollView或者listview内部,第二尽量高度设置具体值或者是match,提高计算高度效率。
performMeasure之后看看performLayout
可以看到host.layout();这个host就是mView,也就是之前setView的时候传递过来的decorView;我们会发现frameLayout中没有重写layout方法,所以直接看viewGroup,发现是final的,并且内部直接调用的spuer.layout。就是View的layout方法。再来看看这个方法
boolean changed = isLayoutModeOptical(mParent) ?
setOpticalFrame(l, t, r, b) : setFrame(l, t, r, b);
其中有这么句,会发现怎么样都会执行setFrame方法,这个方法为view设置了mLeft、mTop等属性,并且会调用invalidate(这个我们之后再继续看)。看来在这个方法里面就已经把view的layout给框好了
再继续看layout()方法,之后还会调用onLayout,这个方法在View里面是空的,然后ViewGroup是个抽象的。子类必须重写,看看frameLayout怎么重写的,就一句话。。。
layoutChildren();跟进去,很重要的。for循环了。依次执行child.layout()。看到这里,我觉得,我们重写onLayout,其实不是框当前这个view的,而是为了循环遍历子view去layout的。在onLayout之前,就已经计算好了位置。我们之前的measure方法测量的大小就是为layout服务的。view的getMeasureWidth这样的方法获得是onMeasure之后测量的大小。而view的getWidth是通过mLeft这样的属性计算出来的,也就是layout之后得到的大小,这2个方法得到的数据可能会不一样。
performDraw
开始要画了直接可以看到调用了draw,然后我们会在最后发现出现了分支
if (mAttachInfo.mHardwareRenderer != null && mAttachInfo.mHardwareRenderer.isEnabled()) {
........//硬件加速开启的时候
}else{
.....
drawSoftware() //软件处理
}
看到这里,就又要引入新的知识了。硬件加速(GPU).
基于软件的绘图模式
基于软件的绘图模式在重绘View时,需要如下两个过程Invalidate the hierarchyDraw the hierarchy
需要进行重绘时,系统发出invalidate()信号,并在View视图树中进行传递,计算需要重新绘制的区域,但是这种绘图方式有两个不足:
当我们只需要重绘视图树中的一个View时,视图树中的View都将进行重绘,而且遍历视图树也浪费大量时间。例如一个ViewA在另一个ViewB之上,即使B没有发生变化,重绘A的时候,B也会重绘。
这种方式隐藏了绘制中的bug,例如上面的例子中,由于ViewA、ViewB相互重叠,有需要重绘的the dirty region,那么如果B忘记了进行重绘的逻辑,那么A进行重绘的时候,就会将B重绘,也就是说使用错误的行为来得到了正确的现象。正是因为这个原因,开发者需要保证在View需要发生重绘时,调用正确的invalidate()方法。
基于硬件的绘图模式
基于硬件的绘图方式同样使用invalidate()信号来进行重绘,但其绘制和渲染的方式不同。Android内部维护一个display list用于记录视图树的显示状态。当收到invalidate()信号时,系统只需要更改需要重绘的视图的display list,而其他未发生改变的视图只需要使用原来的display list即可,整个过程分为三 部分:Invalidate the hierarchyRecord and update display listsDraw the display lists
使用这种方式,就可以避免软件绘图中第二点的bug。
例如,假设有一个包含了一个Button对象的ListView对象的LinearLayout布局,那么LinearLayout布局的显示列表如下:
1. DrawDisplayList(ListView);
2.DrawDisplayList(Button)
假设现在要改变ListView对象的不透明度,那么在调用ListView对象的setAlpha(0.5f)方法时,显示列表就包含了以下处理:
1.SaveLayerAlpha(0.5);
2.DrawDisplayList(ListView);
3. Restore;
4.DrawDisplayList(Button)
View Layers
LAYER_TYPE_NONE:View对象用普通的方式来呈现,并且不是由屏幕外缓存来返回的。这种类型是默认的行为;LAYER_TYPE_HARDWARE:如果应用程序是硬件加速的,那么该View对象被呈现在硬件的一个硬件纹理中。如果没有被硬件加速,那么这种层类型的行为与LAYER_TYPE_SOFTWARE相同。LAYER_TYPE_SOFTWARE: View对象会被呈现在软件的一个位图中。使用哪种层的类型,依赖以下目标:
性能:使用硬件层类型,把View呈现到一个硬件纹理中。一旦该View对象被呈现到一个层中,那么它的绘图代码直到调用该View对象的invalidate()方法时才会被执行。对于某些动画,如alpha动画,就能够直接使用该层,这么做对于GPU来说是非常高效的。 视觉效果:使用硬件或软件层类型和一个Paint对象,能够把一 些特殊的视觉处理应用给一个View对象。例如,使用ColorMatrixColorFilter对象绘制一个黑白相间的View对象。 兼容性:使用软件层类型会强制把一个View对象呈现在软件中。如果View对象被硬件加速(例如,如果整个应用程序都被硬件加速)发生呈现问题,那么使用软件层类型来解决硬件呈现管道的限制是一个容 易的方法。
硬件加速具体使用例子 :动画,我们应用的引导页,动画复杂,多个动画组成,非常的卡,这里就可以利用这个。
View.setLayerType(View.LAYER_TYPE_HARDWARE, null); ObjectAnimator animator = ObjectAnimator.ofFloat(view, "rotationY", 180); animator.addListener(new AnimatorListenerAdapter() { @Override public void onAnimationEnd(Animator animation) { view.setLayerType(View.LAYER_TYPE_NONE, null); } }); animator.start();
实测效果很好,但是有一点要注意,动画结束的时候一定要取消硬件加速,因为硬件加速是非常耗内存的。如果不取消就会导致oom(实测确实出现了)。
提示和技巧
切换到2D图形硬加速能立即提升性能,但是你仍然需要按照以下建议来有效使用GPU:
• 减少你应用中views的数量
系统画的views越多,就越慢.这对软件呈现管线来说也是一样.减少views是优化你的UI的最简单的办法.
• 避免过度绘制
不要在彼此的上面画太多layers.移除那些完全被别的不透明view遮盖的view们.如果你需要把多个layer混合画到每个view的上面,应考虑把它们合并到一个layer中.对于当前硬件的一个好原则是画的次数不要超过每帧屏幕上像素的2.5倍(透明像素按位图计数).
• 不要在绘制方法们中创建render对象
一个常见的错误就是在每次调用绘制方法时都创建新的Paint或Path.这强制垃圾收集器运行得更频繁,并且导致高速缓存和硬件管道优化不起作用.
• 不要太频繁地修改shapes
比如混合shapes,paths,和circles时,是使用纹理遮罩呈现的.每次你创建或修改一个path,硬件管道都创建一个新的遮罩,这个代价是很昂贵的.
• 不要太频繁地修改bitmap
你每次改变一个bitmap的内容,你下次去画它时它会作为一个GPU纹理重新上载.
• 小心使用alpha(透明度)
当你用setAlpha(),AlphaAnimation,或ObjectAnimator把一个view设为透明,它将被呈现到一个离屏缓冲中,此时就需要双倍的填充速率.当在一个very大的view上应用透明度时,应考虑把view的layer类型设置为LAYER_TYPE_HARDWARE.
好了现在我们继续看,我们只关心drawSoftware这个方法,可以在这个方法里看到
......
final Rect dirty = mDirty;
......
canvas = mSurface.lockCanvas(dirty); //基于Surface,以后再学习........
...... mView.draw(canvas); ......//基于canvas,这个以后再学习,又可以画各种各样的东西
好了,这个mView,不用多说了,就是DecorView,一路追下去,看到FrameLayout的draw,发现它会画一个mForeground,如果有的话,这个就是覆盖在所有view之上的一个drawable..(发现新玩意.......也许以后可以用到,毛玻璃效果?),最后就到了View的draw,好吧。。。
在这里可以看到6个步骤!!!!
/*
* 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)
*/
根据源码注释,2&5不是必须的,2&5是如果scroll 渐变效果开着的,就会去画,跳过不看。看1,3,4,6
第一步
drawBackground(canvas);进去看看,发现如果mBackground == null 就return了,后面繁琐的事情不做了,可以想象出为什么我们画布局的时候,减少有交集的background,过渡渲染层会变少。看到这里大家应该清楚了,这个过渡渲染其实跟xml层级木有关系,跟这个background有关系,只要有覆盖,下层视图看不见,但是还是画了,就是过渡渲染。
第三步
onDraw,我们会发现view这个方法是空方法,因为每个view长得不一样,所以需要自己去画,ViewGroup也没有重写这个方法。
第四步
dispatchDraw,viewgroup会重写这个方法,会循环编译childView,调用view.draw方法
第六步
onDrawScrollBars,画滚动条,其实每个view都有滚动条,只是画没画的问题
其实还有个第七步,不过这个只在4.3之上才有的,就是viewoverlay,这是一个在所有页面之上的一个View,没有点击相应事件,可以做一些动画
到这里三部曲结束。
但是我们还要来看看之前留的一个坑-invalidate