Android绘制View相关的几个问题
前面关于View绘制的话题好像零散的写过博客,虽然好久没有认真的研究一些东西了,平时忙其他的东西,但是本着每个月必须花几天时间看看android的想法,今天整理了几个View绘制相关的问题,这里不会涉及View测量布局绘制的那部分细节,因为这些前面已经写过了。主要有以下几个问题
1.View绘制流程 invalidate/requestLayout
2.View树和DecorView
4.真的只能在主线程操作UI吗
5.几个函数:WindowManagere#addView、ViewGroup#addView以及PhoneWindow#addContentView的区别
View绘制流程 invalidate/requestLayout
我们平时要刷新界面的时候会主动调用Invalidate函数,其实一些函数也会间接调用invalidate函数比如,setVisibility等与界面有关的函数都会辗转调用到与invalidate相关的一些函数。当调用invalidate函数更新界面重新绘制时,首先要明确,界面的绘制是由ViewRoot开始的。正常情况下,一个Activity就有一个ViewRoot,当某个控件状态改变后调用invalidate函数会一层一层的调用父控件的方法,直到调用的View树的根,也即ViewRoot。最后ViewRoot会调用scheduleTraversals函数,里面会产生一个异步的绘制事件执行我们都知道的控件绘制相关的三个函数measure、layout和draw函数,简单地说就是某个View要重新绘制时,都是请求它所在的View树根来绘制的。
函数调用流程如图所示,当某个View调用到invalidate函数,它会调用父View(准确的说应该是ViewParent对象)的invalidateChild函数,在调用到invalidateChildInParent函数,在这个函数里面会循环调用上一层父View的invalidateChildInParent函数,这部分所做的处理主要是设置View的mPrivateFlags参数的位标志,计算要绘制的矩形等,直到调用到View树的根。
当某个View就是View树的第一个孩子,它调用invalidate重新绘制时,就直接调用到ViewRoot的invalidateChild函数,也就是图中虚线部分,在ViewRoot中的invalidateChild函数中也仅仅调用了invalidateChildInParent函数。这个View一般来说对应的就是DecorView。
同样还有一个用来绘制的函数requestLayout,这个函数也会导致View树的重新测量、布局,这个函数要比invalidate函数更强一点,前面说的invalidate函数主要作用于draw函数,也就是强制重新绘制当前View,当要绘制的View的大小位置没有变化时是就可以不重新测量和layout布局了,只需要绘制即可,而当使用requestLayout函数时,就会强制测量和重新布局,其实也就是设置了View的mPrivateFlags的某些位的标记。
public void requestLayout() { mPrivateFlags |= PFLAG_FORCE_LAYOUT; mPrivateFlags |= PFLAG_INVALIDATED; if (mParent != null && !mParent.isLayoutRequested()) { mParent.requestLayout(); } }
当调用到measure时由于PFLAG_FORCE_LAYOUT已经被设置,所以就会重新measure,在layout时也是类似的。
public final void measure(int widthMeasureSpec, int heightMeasureSpec) { if ((mPrivateFlags & PFLAG_FORCE_LAYOUT) == PFLAG_FORCE_LAYOUT || widthMeasureSpec != mOldWidthMeasureSpec || heightMeasureSpec != mOldHeightMeasureSpec) { // ... // measure ourselves, this should set the measured dimension flag back onMeasure(widthMeasureSpec, heightMeasureSpec); // ... mPrivateFlags |= PFLAG_LAYOUT_REQUIRED; } mOldWidthMeasureSpec = widthMeasureSpec; mOldHeightMeasureSpec = heightMeasureSpec; }
View树和DecorView
回顾Activity的生命周期onResume,启动一个Activity后最终会调用到ActivityThread中的handleResumeActivity,在里面首先调用performResumeActivity的执行最终会执行到activity的生命周期onResume,接下来会执行View的添加和显示。
if (r.window == null && !a.mFinished && willBeVisible) { r.window = r.activity.getWindow(); View decor = r.window.getDecorView(); decor.setVisibility(View.INVISIBLE); ViewManager wm = a.getWindowManager(); WindowManager.LayoutParams l = r.window.getAttributes(); a.mDecor = decor; l.type = WindowManager.LayoutParams.TYPE_BASE_APPLICATION; l.softInputMode |= forwardBit; if (a.mVisibleFromClient) { a.mWindowAdded = true; wm.addView(decor, l); } }
当调用WindowManager的addView方法时,会调用到WindowManagerImp的addView方法,WindowManagerImp类是对WindowManagerGlobal的封装。
public void addView(View view, ViewGroup.LayoutParams params) { mGlobal.addView(view, params, mDisplay, mParentWindow); } public void addView(View view, ViewGroup.LayoutParams params, Display display, Window parentWindow) { ViewRootImpl root; View panelParentView = null; ...... root = new ViewRootImpl(view.getContext(), display); // 创建一个ViewRoot对象 view.setLayoutParams(wparams); if (mViews == null) { index = 1; mViews = new View[1]; mRoots = new ViewRootImpl[1]; mParams = new WindowManager.LayoutParams[1]; } else { index = mViews.length + 1; Object[] old = mViews; mViews = new View[index]; System.arraycopy(old, 0, mViews, 0, index-1); old = mRoots; mRoots = new ViewRootImpl[index]; System.arraycopy(old, 0, mRoots, 0, index-1); old = mParams; mParams = new WindowManager.LayoutParams[index]; System.arraycopy(old, 0, mParams, 0, index-1); } index--; mViews[index] = view; mRoots[index] = root; mParams[index] = wparams; } try { root.setView(view, wparams, panelParentView);// 调用ViewRoot的setView方法 } // ...... }
在WindowManagerGlobal中mViews存储了添加的View,mRoots存储了ViewRoot,mParams存储了添加该View的布局参数,这三个数组中保存的数据是对应的。然后在setView函数中请求UI重新绘制,并且通过IPC调用通知WindowManagerService添加窗口。
public void setView(View view, WindowManager.LayoutParams attrs, View panelParentView) { // Schedule the first layout -before- adding to the window // manager, to make sure we do the relayout before receiving // any other events from the system. requestLayout(); // 请求UI开始绘制重新绘制View树 // ...... try { mOrigWindowType = mWindowAttributes.type; mAttachInfo.mRecomputeGlobalAttributes = true; collectViewAttributes(); // 通知WindowManagerService添加一个窗口 会调用到addWindow方法 res = mWindowSession.addToDisplay(mWindow, mSeq, mWindowAttributes, getHostVisibility(), mDisplay.getDisplayId(), mAttachInfo.mContentInsets, mInputChannel); } //… //指定了该View的父亲(其实是ViewParent对象)是新创建的ViewRoot view.assignParent(this); //… }
至此就是系统启动一个Activity时将其DecorView添加到View树的过程,总结起来就是在Activity执行完onResume后,创建View树,将DecorView通过IPC调用WindowManagerService的addView函数添加到窗口,并且调用requestLayout函数请求绘制窗口,注意到在ViewRootImp#setView函数中调用view.assignParent(this);也即指定了顶层DecorView的ViewParent为ViewRootImp对象。
上面提到的ViewRoot均为ViewRootImp类,旧版本中确实有这个类,但在新版本的Android源码中改名变成了ViewRootImp类,它是实现了ViewParent接口的,并不是一个View。所有的ViewGroup也实现了ViewParent,每个View都在内部保存了一个ViewParent对象,来表示他的上层”父View”,所以对于最顶层的DecorView来说它的mParent变量指的就是一个ViewRootImp对象,即这棵View树的树根。
真的不能在子线程更新UI吗
在上面看到调用了ViewRootImp的requestLayout函数,他在执行绘制View树之前调用了一个非常重要的函数,也就是checkThread函数。在调用invalidate函数时也是一样的,当调用到ViewRootImp的invalidateChildInParent函数时,也首先调用了checkThread函数。
public void requestLayout() { checkThread(); mLayoutRequested = true; scheduleTraversals(); }
我们看一下checkThread函数,发现抛出的这句异常经常在开发的时候遇到,当在子线程中更新UI就会提示这句话,显然这里意思是说:只有创建这个View继承关系的线程才能修改这个View。
void checkThread() { if (mThread != Thread.currentThread()) { throw new CalledFromWrongThreadException( "Only the original thread that created a view hierarchy can touch its views."); } }这里面也只是简单的比较了mThread的值和当前线程是否相同,而mThread的赋值是在ViewRootImp的构造函数里面。
public ViewRootImpl(Context context, Display display) { super(); //… mThread = Thread.currentThread(); // … }
那么,我们就知道了,在刷新View的时候执行checkThread并不一定是说再检查我们是不是在UI线程修改。而是说现在修改的这个View他所在的View树的根创建的线程是否和当前操作View的线程一样。
当然了,DecorView所在的View树就是在UI线程中创建的,因此大多数时候我们操作的View都在这棵树下面,造成的结果是只能在创建DecorView的ViewRoot的线程也就是UI线程中修改View。
那么有两个方面可以考虑,一方面这个View树是在onResume后才创建的,那么在此之前修改UI由于当前的View树的树根还不存在,因此暂时不会绘制界面,只会保存设置的状态,在下次请求绘制UI时会再次刷新UI。另一方面,正如上面添加DecorView的方式既然WindowManager#addView方法可以创建一个ViewRoot,那么通过这种方式来修改界面就可以有自己的View树,也就不受限于主线程中修改UI了,可以看下面的测试可以正常的运行。
protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); tv = (TextView)findViewById(R.id.tv); new Thread() { public void run(){ tv.setText("change text in non-UI Thread"); } }.start(); }
上面的代码可以正常的运行,而下面的代码就会报异常。由于睡眠两秒钟后才更新UI,这段时间内早已完成了前期的初始化,onResume也已经执行完成,有了自己的View树,当更新View状态时会进行线程检查的。
protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); tv = (TextView)findViewById(R.id.tv); new Thread() { public void run(){ try { Thread.sleep(2000); } catch (InterruptedException e) { e.printStackTrace(); } tv.setText("change text in non-UI Thread"); } }.start(); }
创建自己的View树,这种情况在悬浮框效果中用到过,只不过我们这里放在子线程中。如下面简单代码在子线程中调用,效果为在屏幕显示一幅图片,达到了在子线程操作UI的效果。
private void viewInOtherThread(){ view=new View(getApplicationContext()); view.setBackgroundResource(R.drawable.ic_launcher); mWindowManager=(WindowManager) getApplicationContext().getSystemService(Context.WINDOW_SERVICE); WindowManager.LayoutParams param=new WindowManager.LayoutParams(); param.type=WindowManager.LayoutParams.TYPE_SYSTEM_ALERT; param.format=1; param.flags = WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE; param.flags = param.flags | WindowManager.LayoutParams.FLAG_WATCH_OUTSIDE_TOUCH; param.flags = param.flags | WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS; param.alpha = 1.0f; param.width=200; param.height=200; mWindowManager.addView(view, param); }
需要注意的是,上面的viewInOtherThread在子线程中调用时需要创建Looper,因为ViewRootImp这个对象中包含一个mHandler也即一个Handler对象,因此需要一个Looper,另外在activity销毁时记得移除这个view,并且记得添加权限.
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" />
添加View的几个方法的区别
提到View树,顺便可以探讨的是以下几个方法:WindowManagere#addView、ViewGroup#addView以及Activity#addContentView
考虑下面的例子,XML文件很简单,就是一个垂直线性布局里面有一个TextView。为了清除起见,设置主题为没有标题栏。
<?xml version="1.0" encoding="utf-8"?> <LinearLayout android:id="@+id/ll" xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical"> <TextView android:id="@+id/tv" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="test"/> </LinearLayout>
这时View的界面层次如图
当使用前面WindowManagere#addView方式添加的View,会创建一个新的View树,使用HierarchyView工具看不出来,但至少没有在原始的View树上看到新增的View。
使用ViewGroup#addView方法,在当前线性布局中添加新的View,View树如下图所示,即这个View添加到已有的View树上,也就是DecorView所在的树上。
使用Activity#addContentView方式添加一个新的View,View树如下图所示,显然新添加的View是处在和@id/ll这个布局同一个层级上,也即android:id/content的子View,这从该函数的名字也可以清除的看出来。
setContentView会覆盖掉android:id/content下面的View,前面调用设置过的View被后面的覆盖掉了
查看原文:http://qhyuang1992.com/index.php/2016/06/21/android_hui_zhi_view_xiang_guan_de_ji_ge_wen_ti/