GVKun编程网logo

Android-ViewRootImpl $ CalledFromWrongThreadException

12

想了解Android-ViewRootImpl$CalledFromWrongThreadException的新动态吗?本文将为您提供详细的信息,此外,我们还将为您介绍关于Androidandroid

想了解Android-ViewRootImpl $ CalledFromWrongThreadException的新动态吗?本文将为您提供详细的信息,此外,我们还将为您介绍关于Android android.view.ViewRoot$CalledFromWrongThreadException异常的解决方案、Android DecorView与ViewRootImpl、Android View 的绘制流程之 Measure 过程详解 (一) Android View 绘制流程之 DecorView 与 ViewRootImplAndroid View 的绘制流程之 Measure 过程详解 (一)Android View 的绘制流程之 Layout 和 Draw 过程详解 (二)Android View 的事件分发原理解析Android 自定义 View 详解Android View 绘制流程之 DecorView 与 ViewRootImp、Android View 绘制流程之 DecorView 与 ViewRootImpl的新知识。

本文目录一览:

Android-ViewRootImpl $ CalledFromWrongThreadException

Android-ViewRootImpl $ CalledFromWrongThreadException

我正在使用它来显示来自互联网的图像,但它引发如下错误:
04-12 13:45:05.337:E /
AndroidRuntime(27897):由以下原因引起:android.view.ViewRootImpl $
CalledFromWrongThreadException:仅原始线程创建视图层次结构的对象可以触摸其视图。

public class Order extends Activity {        @Override        public void onCreate(Bundle savedInstanceState) {            super.onCreate(savedInstanceState);            new DownloadFilesTask().execute();               }            private class DownloadFilesTask extends AsyncTask<Void, Void, Void> {            protected void onPostExecute(Void result) {            }             @Override             protected Void doInBackground(Void... params) {                 setContentView(R.layout.order);                    ImageView imageView = (ImageView)findViewById(R.id.imgView);                      imageView.setImageDrawable(createDrawableFromURL("http://savagelook.com/misc/sl_drop2.png"));                    return null;             }                  }             private Drawable createDrawableFromURL(String urlString) {            Drawable image = null;        try {            URL url = new URL(urlString);            InputStream is = (InputStream)url.getContent();            image = Drawable.createFromStream(is, "src");        } catch (MalformedURLException e) {            image = null;        } catch (IOException e) {            image = null;        }         return image;        }}

答案1

小编典典

放进去 onCreate()

ImageView imageView;    @Override    public void onCreate(Bundle savedInstanceState) {        super.onCreate(savedInstanceState);        setContentView(R.layout.order);        imageView = (ImageView)findViewById(R.id.imgView);        new DownloadFilesTask().execute();           }

你的AsyncTask课应该是这样的

        private class DownloadFilesTask extends AsyncTask<Void, Void, Void> {             Drawable drawable;             @Override             protected Void doInBackground(Void... params) {             drawable = createDrawableFromURL(                                   "http://savagelook.com/misc/sl_drop2.png");              return null;             }             protected void onPostExecute(Void result) {                    imageView.setImageDrawable(drawable);            }                  }

Android android.view.ViewRoot$CalledFromWrongThreadException异常的解决方案

Android android.view.ViewRoot$CalledFromWrongThreadException异常的解决方案

android.view.ViewRoot$CalledFromWrongThreadException异常的解决方案

在Android平台下,进行多线程编程时,经常需要在主线程之外的一个单独的线程中进行某些处理,然后更新用户界面显示。但是,在主线线程之外的线程中直接更新页面显示的问题是

 

报异常:android.view.ViewRoot$CalledFromWrongThreadException: Only the original thread that created a view hierarchy can touch its views.

【只有原始创建这个视图层次(view hierachy)的线程才能修改它的视图(view)】

也就是说必须在一般必须在程序的主线程(也就是ui)线程中进行更新界面显示的工作。可以采用下面的方法之一来解决:

法1:

在Activity.onCreate(Bundle savedInstanceState)中创建一个Handler类的实例, 在这个Handler实例的handleMessage回调函数中调用更新界面显示的函数。

/** 
     * 启动线程用来刷新登录提示文字,N秒刷新一次 
     *  
     */  
    private class FreshWordsThread extends Thread  
    {  
        @Override  
        public void run()  
        {  
            try  
            {  
                mLoadingWords = "test";  
                mLoadhandler.sendEmptyMessage(REFRESH);  
            }  
            catch (InterruptedException e)  
            {  
                e.printStackTrace();  
                Thread.currentThread().interrupt();  
            }  
        }  
    }  
  
  
  
    //主线程中的handler  
    class LoadHandler extends Handler  
    {  
        /** 
         * 接受子线程传递的消息机制 
         */  
        @Override  
        public void handleMessage(Message msg)  
        {  
            super.handleMessage(msg);  
            int what = msg.what;  
  
            Log.i(TAG, "Main handler message code: " + what);  
            switch (what)  
            {                  
                case REFRESH:  
                {  
                    // 刷新页面的文字  
                    mLoadingText.setText(mLoadingWords);  
                    break;  
                }  
  
            }  
        }  
        
    }  

法2:利用Activity.runOnUiThread(Runnable)把更新ui的代码创建在Runnable中,然后在需要更新ui时,把这个Runnable对象传给Activity.runOnUiThread(Runnable)。 这样Runnable对像就能在ui程序中被调用。

FusionField.currentActivity.runOnUiThread(new Runnable() {  
	public void run() {  
		Toast.makeText(FusionField.currentActivity, "Success", Toast.LENGTH_LONG).show();  
	}  
}); 

 

Android DecorView与ViewRootImpl

Android DecorView与ViewRootImpl

结构关系

ViewRootImpl单独是一个普通类,不是视图类。没有继承View。

DecorView是PhoneWindow的一个内部类,是一个视图类继承FrageLayout(ViewGroup).

ViewRootImpl.java
public final class ViewRootImpl implements ViewParent,
        View.AttachInfo.Callbacks, HardwareRenderer.HardwareDrawCallbacks {
    //...
    ...
}


PhoneWindow.java
public class PhoneWindow extends Window implements MenuBuilder.Callback {
    
    private final class DecorView extends FrameLayout implements RootViewSurfaceTaker {
      //...
      ...
    }
   
}

Window.java
public abstract class Window {
    //
    ...
}

 

    // This is the view in which the window contents are placed. It is either
    // mDecor itself, or a child of mDecor where the contents go.
    private ViewGroup mContentParent;

    private void installDecor() {
        if (mDecor == null) {
            mDecor = generateDecor();
            mDecor.setDescendantFocusability(ViewGroup.FOCUS_AFTER_DESCENDANTS);
            mDecor.setIsRootNamespace(true);
            if (!mInvalidatePanelMenuPosted && mInvalidatePanelMenuFeatures != 0) {
                mDecor.postOnAnimation(mInvalidatePanelMenuRunnable);
            }
        }
        if (mContentParent == null) {
            mContentParent = generateLayout(mDecor);

            ...
        }
    }

 

Android View 的绘制流程之 Measure 过程详解 (一) Android View 绘制流程之 DecorView 与 ViewRootImplAndroid View 的绘制流程之 Measure 过程详解 (一)Android View 的绘制流程之 Layout 和 Draw 过程详解 (二)Android View 的事件分发原理解析Android 自定义 View 详解Android View 绘制流程之 DecorView 与 ViewRootImp

Android View 的绘制流程之 Measure 过程详解 (一) Android View 绘制流程之 DecorView 与 ViewRootImplAndroid View 的绘制流程之 Measure 过程详解 (一)Android View 的绘制流程之 Layout 和 Draw 过程详解 (二)Android View 的事件分发原理解析Android 自定义 View 详解Android View 绘制流程之 DecorView 与 ViewRootImp

View 的绘制系列文章:

  • Android View 绘制流程之 DecorView 与 ViewRootImpl

  • Android View 的绘制流程之 Measure 过程详解 (一)

  • Android View 的绘制流程之 Layout 和 Draw 过程详解 (二)

  • Android View 的事件分发原理解析

  • Android 自定义 View 详解

 

概述

上一篇 Android View 绘制流程之 DecorView 与 ViewRootImpl 分析了在调用 setContentView 之后,DecorView 是如何与 activity 关联在一起的,最后讲到了 ViewRootImpl 开始绘制的逻辑。本文接着上篇,继续往下讲,开始分析 view 的绘制流程。

上文说到了调用 performTraversals 进行绘制,由于 performTraversals 方法比较长,看一个简化版:

// ViewRootImpl 类
private void performTraversals() {
     这个方法代码非常多,但是重点就是执行这三个方法
     执行测量
    performMeasure(childWidthMeasureSpec,childHeightMeasureSpec);
     执行布局(ViewGroup)中才会有
    performlayout(lp,mWidth,mHeight);
     执行绘制
    performDraw();
}

其流程具体如下:

 View的整个绘制流程可以分为以下三个阶段:

  • measure: 判断是否需要重新计算 View 的大小,需要的话则计算;

  • layout: 判断是否需要重新计算 View 的位置,需要的话则计算;

  • draw: 判断是否需要重新绘制 View,需要的话则重绘制。

MeasureSpec

在介绍绘制前,先了解下 MeasureSpec。MeasureSpec 封装了父布局传递给子布局的布局要求,它通过一个 32 位 int 类型的值来表示,该值包含了两种信息,高两位表示的是 SpecMode(测量模式),低 30 位表示的是 Specsize(测量的具体大小)。下面通过注释的方式来分析来类:

/**  
 * 三种SpecMode: 
 * 1.UNSPECIFIED 
 * 父 ViewGroup 没有对子View施加任何约束,子 view 可以是任意大小。这种情况比较少见,主要用于系统内部多次measure的情形,
* 用到的一般都是可以滚动的容器中的子view,比如ListView、GridView、RecyclerView中某些情况下的子view就是这种模式。
* 一般来说,我们不需要关注此模式。  * 2.EXACTLY   * 该 view 必须使用父 ViewGroup 给其指定的尺寸。对应 match_parent 或者具体数值(比如30dp)  * 3.AT_MOST   * 该 View 最大可以取父ViewGroup给其指定的尺寸。对应wrap_content  *    * MeasureSpec使用了二进制去减少对象的分配。   
*/   public class MeasureSpec {            进位大小为2的30次方(int的大小为32位,所以进位30位就是要使用int的最高位和第二高位也就是32和31位做标志位)           private static final int MODE_SHIFT = 30;            运算遮罩,0x3为16进制,10进制为3,二进制为11。3向左进位30,就是11 00000000000(11后跟30个0)            (遮罩的作用是用1标注需要的值,0标注不要的值。因为1与任何数做与运算都得任何数,0与任何数做与运算都得0)           int MODE_MASK  = 0x3 << MODE_SHIFT;            0向左进位30,就是00 00000000000(00后跟30个0)           int UNSPECIFIED = 0 << MODE_SHIFT;            1向左进位30,就是01 00000000000(01后跟30个0)           int EXACTLY     = 1 << 2向左进位30,就是10 00000000000(10后跟30个0)           int AT_MOST     = 2 <<           * 根据提供的size和mode得到一个详细的测量结果                       第一个return:          measureSpec = size + mode;   (注意:二进制的加法,不是十进制的加法!)            这里设计的目的就是使用一个32位的二进制数,32和31位代表了mode的值,后30位代表size的值            例如size=100(4),mode=AT_MOST,则measureSpec=100+10000...00=10000..00100                      第二个return:          size &; ~MODE_MASK就是取size 的后30位,mode &amp; MODE_MASK就是取mode的前两位,最后执行或运算,得出来的数字,前面2位包含代表mode,后面30位代表size         int makeMeasureSpec(int size, int mode) {               if (sUsebrokenMakeMeasureSpec) {                 return size + mode;             } else {                 return (size & ~MODE_MASK) | (mode &  MODE_MASK);             }         }                      * 获得SpecMode           mode = measureSpec &amp; MODE_MASK;            MODE_MASK = 11 00000000000(11后跟30个0),原理是用MODE_MASK后30位的0替换掉measureSpec后30位中的1,再保留32和31位的mode值。            例如10 00..00100 & 11 00..00(11后跟30个0) = 10 00..00(AT_MOST),这样就得到了mode的值           int getMode( measureSpec) {               return (measureSpec & MODE_MASK);           }                      * 获得Specsize            size = measureSpec &  ~MODE_MASK;            原理同上,不过这次是将MODE_MASK取反,也就是变成了00 111111(00后跟30个1),将32,31替换成0也就是去掉mode,保留后30位的size           int getSize(return (measureSpec &  ~MODE_MASK);           }   }  

顺便提下 MATCH_PARENT 和 WRAP_CONTENT 这两个代表的值,分别是 -1 和 -2。

       
         * Special value for the height or width requested by a View.
         * MATCH_PARENT means that the view wants to be as big as its parent,* minus the parent''s padding,if any. Introduced in API Level 8.
         */
        public static final int MATCH_PARENT = -1;

        
         * Special value for the height or width requested by a View.
         * WRAP_CONTENT means that the view wants to be just large enough to fit
         * its own internal content,taking its own padding into account.
         int WRAP_CONTENT = -2;

Decorview 尺寸的确定

在 performTraversals 中,首先是要确定 DecorView 的尺寸。只有当 DecorView 尺寸确定了,其子 View 才可以知道自己能有多大。具体是如何去确定的,可以看下面的代码:

Activity窗口的宽度和高度
 desiredWindowWidth;
 desiredWindowHeight;
...
用来保存窗口宽度和高度,来自于全局变量mWinFrame,这个mWinFrame保存了窗口最新尺寸
Rect frame = mWinFrame;
构造方法里mFirst赋值为true,意思是第一次执行遍历吗    
 (mFirst) {
    是否需要重绘
    mFullRedrawNeeded = true;
    是否需要重新确定Layout
    mLayoutRequested = ;
    
    // 这里又包含两种情况:是否包括状态栏
    
    判断要绘制的窗口是否包含状态栏,有就去掉,然后确定要绘制的Decorview的高度和宽度
     (shouldUsedisplaySize(lp)) {
         NOTE -- system code,won''t try to do compat mode.
        Point size = new Point();
        mdisplay.getRealSize(size);
        desiredWindowWidth = size.x;
        desiredWindowHeight = size.y;
    }  {
        宽度和高度为整个屏幕的值
        Configuration config = mContext.getResources().getConfiguration();
        desiredWindowWidth = dipToPx(config.screenWidthDp);
        desiredWindowHeight = dipToPx(config.screenHeightDp);
    }
    ...
 else{
    
        // 这是window的长和宽改变了的情况,需要对改变的进行数据记录
    
        如果不是第一次进来这个方法,它的当前宽度和高度就从之前的mWinFrame获取
        desiredWindowWidth = frame.width();
        desiredWindowHeight = frame.height();
        
         * mWidth和mHeight是由WindowManagerService服务计算出的窗口大小,
         * 如果这次测量的窗口大小与这两个值不同,说明WMS单方面改变了窗口的尺寸
         if (desiredWindowWidth != mWidth || desiredWindowHeight != mHeight) {
            if (DEBUG_ORIENTATION) Log.v(mTag,"View " + host + " resized to: " + frame);
            需要进行完整的重绘以适应新的窗口尺寸
            mFullRedrawNeeded = ;
            需要对控件树进行重新布局
            mLayoutRequested = window窗口大小改变
            windowSizeMayChange = ;
        }
 }
    ...
    // 进行预测量
     (layoutRequested){
        ...
         (mFirst) {
             视图窗口当前是否处于触摸模式。
            mAttachInfo.mInTouchMode = !mAddedTouchMode;
            确保这个Window的触摸模式已经被设置
            ensuretouchModeLocally(mAddedTouchMode);
        }  {
            六个if语句,判断insects值和上一次比有什么变化,不同的话就改变insetsChanged
            insects值包括了一些屏幕需要预留的区域、记录一些被遮挡的区域等信息
            if (!mPendingOverscanInsets.equals(mAttachInfo.mOverscanInsets)) {
                    insetsChanged = ;
            }
            ...
            
          //  这里有一种情况,我们在写dialog时,会手动添加布局,当设定宽高为Wrap_content时,会把屏幕的宽高进行赋值,给出尽量长的宽度
            
            
             * 如果当前窗口的根布局的width或height被指定为 WRAP_CONTENT 时,
             * 比如Dialog,那我们还是给它尽量大的长宽,这里是将屏幕长宽赋值给它
             */
            if (lp.width == ViewGroup.LayoutParams.WRAP_CONTENT
                    || lp.height == ViewGroup.LayoutParams.WRAP_CONTENT) {
                windowSizeMayChange = ;
                判断要绘制的窗口是否包含状态栏,有就去掉,然后确定要绘制的Decorview的高度和宽度
                 (shouldUsedisplaySize(lp)) {
                     Point();
                    mdisplay.getRealSize(size);
                    desiredWindowWidth = size.x;
                    desiredWindowHeight = size.y;
                }  {
                    Configuration config = res.getConfiguration();
                    desiredWindowWidth = dipToPx(config.screenWidthDp);
                    desiredWindowHeight = dipToPx(config.screenHeightDp);
                }
            }
        }
    }
}

这里主要是分两步走:

  1. 如果是第一次测量,那么根据是否有状态栏,来确定是直接使用屏幕的高度,还是真正的显示区高度。

  2. 如果不是第一次,那么从 mWinFrame 获取,并和之前保存的长宽高进行比较,不相等的话就需要重新测量确定高度。

当确定了 DecorView 的具体尺寸之后,然后就会调用 measureHierarchy 来确定其 MeasureSpec :

  Ask host how big it wants to be
  windowSizeMayChange |= measureHierarchy(host,lp,res,desiredWindowWidth,desiredWindowHeight); 

其中 host 就是 DecorView,lp 是 wm 在添加时候传给 DecorView 的,最后两个就是刚刚确定显示宽高 ,看下方法的具体逻辑 :

    boolean measureHierarchy(final View host,final WindowManager.LayoutParams lp,final Resources res,1)">int desiredWindowWidth,1)"> desiredWindowHeight) {
         childWidthMeasureSpec;
         childHeightMeasureSpec;
        boolean windowSizeMayChange = false;boolean goodMeasure = ;
     // 说明是 dialog
ViewGroup.LayoutParams.WRAP_CONTENT) { On large screens,we don''t want to allow dialogs to just stretch to fill the entire width of the screen to display one line of text. First try doing the layout at a smaller size to see if it will fit. final displayMetrics packageMetrics = res.getdisplayMetrics(); res.getValue(com.android.internal.R.dimen.config_prefDialogWidth,mTmpValue,1)">); int baseSize = 0       // 获取一个基本的尺寸 if (mTmpValue.type == TypedValue.TYPE_DIMENSION) { baseSize = ()mTmpValue.getDimension(packageMetrics); } if (DEBUG_DIALOG) Log.v(mTag,"Window " + mView + ": baseSize=" + baseSize + ",desiredWindowWidth=" + desiredWindowWidth);
       // 如果大于基本尺寸
if (baseSize != 0 && desiredWindowWidth > baseSize) { childWidthMeasureSpec = getRootMeasureSpec(baseSize,lp.width); childHeightMeasureSpec = getRootMeasureSpec(desiredWindowHeight,lp.height); performMeasure(childWidthMeasureSpec,childHeightMeasureSpec); host.getMeasuredHeight() + ") from width spec: " + MeasureSpec.toString(childWidthMeasureSpec) + " and height spec: " + MeasureSpec.toString(childHeightMeasureSpec));
          // 判断测量是否准确
if ((host.getMeasuredWidthAndState()&View.MEASURED_STATE_TOO_SMALL) == 0) { goodMeasure = ; } { Didn''t fit in that size... try expanding a bit. baseSize = (baseSize+desiredWindowWidth)/2; baseSize); childWidthMeasureSpec = performMeasure(childWidthMeasureSpec,childHeightMeasureSpec); ); ) { ); goodMeasure = ; } } } }      // 这里就是一般 DecorView 会走的逻辑 goodMeasure) { childWidthMeasureSpec = getRootMeasureSpec(desiredWindowWidth,lp.width); childHeightMeasureSpec = performMeasure(childWidthMeasureSpec,childHeightMeasureSpec);
       // 与之前的尺寸进行对比,看看是否相等,不想等,说明尺寸可能发生了变化
if (mWidth != host.getMeasuredWidth() || mHeight != host.getMeasuredHeight()) { windowSizeMayChange = ; } } return windowSizeMayChange; }

上面主要主要做的就是来确定父 View 的 MeasureSpec。但是分了两种不同类型:

  • 如果宽是 WRAP_CONTENT 类型,说明这是 dialog,会有一些针对 dialog 的处理,最终会调用 performMeasure 进行测量;

  • 对于一般 Activity 的尺寸,会调用  getRootMeasureSpec MeasureSpec 。

下面看下 DecorView MeasureSpec 的计算方法:

    int getRootMeasureSpec(int windowSize,1)"> rootDimension) {
         measureSpec;
        switch (rootDimension) {

        case ViewGroup.LayoutParams.MATCH_PARENT:
             Window can''t resize. Force root view to be windowSize.
            measureSpec = MeasureSpec.makeMeasureSpec(windowSize,MeasureSpec.EXACTLY);
            break;
         ViewGroup.LayoutParams.WRAP_CONTENT:
             Window can resize. Set max size for root view.
            measureSpec =default:
             Window wants to be an exact size. Force root view to be that size.
            measureSpec = MeasureSpec.makeMeasureSpec(rootDimension,1)">;
        }
        return measureSpec;
    }

 该方法主要是根据 View 的 MeasureSpec 是根据宽高的参数来划分的。

  • MATCH_PARENT :精确模式,大小就是窗口的大小;

  • WRAP_CONTENT :最大模式,大小不定,但是不能超过窗口的大小;

  • 固定大小:精确模式,大小就是指定的具体宽高,比如100dp。

对于 DecorView 来说就是走第一个 case,到这里 DecorView 的 MeasureSpec 就确定了,从 MeasureSpec 可以得出 DecorView 的宽高的约束信息。

获取子 view 的 MeasureSpec

当父 ViewGroup 对子 View 进行测量时,会调用 View 类的 measure 方法,这是一个 final 方法,无法被重写。ViewGroup 会传入自己的 widthMeasureSpec 和  heightMeasureSpec,分别表示父 View 对子 View 的宽度和高度的一些限制条件。尤其是当 ViewGroup 是 WRAP_CONTENT 的时候,需要优先测量子 View,只有子 View 宽高确定,ViewGroup 才能确定自己到底需要多大的宽高。

当 DecorView 的 MeasureSpec 确定以后,ViewRootImpl 内部会调用 performMeasure 方法:

  void performMeasure(int childWidthMeasureSpec,1)"> childHeightMeasureSpec) {
        if (mView == null) {
            ;
        }
        Trace.traceBegin(Trace.TRACE_TAG_VIEW,"measure");
        try {
            mView.measure(childWidthMeasureSpec,childHeightMeasureSpec);
        } finally {
            Trace.traceEnd(Trace.TRACE_TAG_VIEW);
        }
    }

该方法传入的是对 DecorView 的 MeasureSpec,其中 mView 就是 DecorView 的实例,接下来看 measure() 的具体逻辑:


 * 调用这个方法来算出一个View应该为多大。参数为父View对其宽高的约束信息。
 * 实际的测量工作在onMeasure()方法中进行
 */
void measure(int widthMeasureSpec,1)"> heightMeasureSpec) {
  ......
  // Suppress sign extension for the low bytes
   long key = (long) widthMeasureSpec << 32 | (long) heightMeasureSpec & 0xffffffffL;
   if (mMeasureCache == null) mMeasureCache = new LongSparseLongArray(2);
 若mPrivateFlags中包含PFLAG_FORCE_LAYOUT标记,则强制重新布局
   比如调用View.requestLayout()会在mPrivateFlags中加入此标记
  boolean forceLayout = (mPrivateFlags & PFLAG_FORCE_LAYOUT) == PFLAG_FORCE_LAYOUT;
  boolean specChanged = widthMeasureSpec != mOldWidthMeasureSpec
      || heightMeasureSpec != mOldHeightMeasureSpec;
  boolean isspecExactly = MeasureSpec.getMode(widthMeasureSpec) == MeasureSpec.EXACTLY
      && MeasureSpec.getMode(heightMeasureSpec) == MeasureSpec.EXACTLY;
  boolean matchesSpecsize = getMeasuredWidth() == MeasureSpec.getSize(widthMeasureSpec)
      && getMeasuredHeight() == MeasureSpec.getSize(heightMeasureSpec);
  boolean needsLayout = specChanged
      && (sAlwaysRemeasureExactly || !isspecExactly || !matchesSpecsize);

   需要重新布局  
  if (forceLayout || needsLayout) {
    // first clears the measured dimension flag 标记为未测量状态
mPrivateFlags &= ~PFLAG_MEASURED_DIMENSION_SET;
  // 对阿拉伯语、希伯来语等从右到左书写、布局的语言进行特殊处理
resolveRtlPropertiesIfNeeded();
 先尝试从缓从中获取,若forceLayout为true或是缓存中不存在或是
     忽略缓存,则调用onMeasure()重新进行测量工作
    int cacheIndex = forceLayout ? -1 : mMeasureCache.indexOfKey(key);
    if (cacheIndex < 0 || sIgnoreMeasureCache) {
       measure ourselves,this should set the measured dimension flag back
      onMeasure(widthMeasureSpec,heightMeasureSpec);
      . . .
    }  {
       缓存命中,直接从缓存中取值即可,不必再测量
      long value = mMeasureCache.valueAt(cacheIndex);
       Casting a long to int drops the high 32 bits,no mask needed
      setMeasuredDimensionRaw((int) (value >> 32),() value);
mPrivateFlags3 |= PFLAG3_MEASURE_NEEDED_BEFORE_LAYOUT;
    }
   // 如果自定义的View重写了onMeasure方法,但是没有调用setMeasuredDimension()方法就会在这里抛出错误;
   // flag not set,setMeasuredDimension() was not invoked,we raise
   // an exception to warn the developer
   if ((mPrivateFlags & PFLAG_MEASURED_DIMENSION_SET) != PFLAG_MEASURED_DIMENSION_SET) {
   throw new IllegalStateException("View with id " + getId() + ": "
  + getClass().getName() + "#onMeasure() did not set the"
  + " measured dimension by calling"
  + " setMeasuredDimension()");
   } 
      //到了这里,View已经测量完了并且将测量的结果保存在View的mMeasuredWidth和mMeasuredHeight中,将标志位置为可以layout的状态

    mPrivateFlags |= PFLAG_LAYOUT_required;

  }
  mOldWidthMeasureSpec = widthMeasureSpec;
  mOldHeightMeasureSpec = heightMeasureSpec;
// 保存到缓存中 mMeasureCache.put(key,((
long) mMeasuredWidth) << 32 | (long) mMeasuredHeight & 0xffffffffL); suppress sign extension }

 这里要注意的是,这是一个 final 方法,不能被继承。这个方法只在 View 类里面。总结一下 measure() 都干了什么事:

  • 调用 View.measure()方法时 View 并不是立即就去测量,而是先判断一下要不要进行测量操作,如果没必要,那么 View 就不需要重新测量了,避免浪费时间资源

  • 如果需要测量,在测量之前,会先判断是否存在缓存,存在直接从缓存中获取就可以了,再调用一下 setMeasuredDimensionRaw 方法,将从缓存中读到的测量结果保存到成员变量 mMeasuredWidth 和 mMeasuredHeight 中。

  • 如果不能从 mMeasureCache 中读到缓存过的测量结果,调用 onMeasure() 方法去完成实际的测量工作,并且将尺寸限制条件 widthMeasureSpec 和 heightMeasureSpec 传递给 onMeasure() 方法。关于 onMeasure() 方法,会在下面详细介绍。

  • 将结果保存到 mMeasuredWidth 和 mMeasuredHeight 这两个成员变量中,同时缓存到成员变量 mMeasureCache 中,以便下次执行 measure() 方法时能够从其中读取缓存值。

  • 需要说明的是,View 有一个成员变量 mPrivateFlags,用以保存 View 的各种状态位,在测量开始前,会将其设置为未测量状态,在测量完成后会将其设置为已测量状态。

DecorView 是 FrameLayout 子类,这时候应该去看 FrameLayout 中的 onMeasure() 方法,代码具体如下:

 protected void onMeasure( heightMeasureSpec) {
     // 获取子view的个数
int count = getChildCount(); boolean measureMatchParentChildren = MeasureSpec.getMode(widthMeasureSpec) != MeasureSpec.EXACTLY || MeasureSpec.getMode(heightMeasureSpec) != MeasureSpec.EXACTLY; mMatchParentChildren.clear(); int maxHeight = 0int maxWidth = 0int childState = 0for (int i = 0; i < count; i++final View child = getChildAt(i);
       // mMeasureAllChildren 默认为FALSE,表示是否全部子 view 都要测量,子view不为GONE就要测量
if (mMeasureAllChildren || child.getVisibility() != GONE) {
// 测量子view measureChildWithMargins(child,widthMeasureSpec,
0,heightMeasureSpec,0);
// 获取子view的布局参数
final LayoutParams lp = (LayoutParams) child.getLayoutParams();
// 记录子view的最大宽度和高度 maxWidth
= Math.max(maxWidth,child.getMeasuredWidth() + lp.leftMargin + lp.rightMargin); maxHeight = Math.max(maxHeight,child.getMeasuredHeight() + lp.topMargin + lp.bottomMargin); childState = combineMeasuredStates(childState,child.getMeasuredState());
// 记录所有跟父布局有着相同宽或高的子view
(measureMatchParentChildren) { if (lp.width == LayoutParams.MATCH_PARENT || lp.height == LayoutParams.MATCH_PARENT) { mMatchParentChildren.add(child); } } } } Account for padding too 子view的最大宽高计算出来后,还要加上父View自身的padding maxWidth += getPaddingLeftWithForeground() + getPaddingRightWithForeground(); maxHeight += getPaddingTopWithForeground() + getPaddingBottomWithForeground(); Check against our minimum height and width maxHeight = Check against our foreground''s minimum height and width final Drawable drawable = getForeground(); if (drawable != ) { maxHeight =     // 确定父 view 的宽高 setMeasuredDimension(resolveSizeAndState(maxWidth,childState),resolveSizeAndState(maxHeight,childState << MEASURED_HEIGHT_STATE_SHIFT)); count = mMatchParentChildren.size(); if (count > 1) { mMatchParentChildren.get(i); final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams(); childWidthMeasureSpec;
          // 如果子view的宽是MATCH_PARENT,那么宽度 = 父view的宽 - 父Padding - 子Margin
LayoutParams.MATCH_PARENT) { int width = Math.max(0,getMeasuredWidth() - getPaddingLeftWithForeground() - getPaddingRightWithForeground() - lp.leftMargin - lp.rightMargin); childWidthMeasureSpec = MeasureSpec.makeMeasureSpec( width,MeasureSpec.EXACTLY); } { childWidthMeasureSpec = getChildMeasureSpec(widthMeasureSpec,getPaddingLeftWithForeground() + getPaddingRightWithForeground() + lp.leftMargin + lp.rightMargin,lp.width); } childHeightMeasureSpec;
          // 同理
if (lp.height ==int height = Math.max(0 getPaddingBottomWithForeground() - lp.topMargin - lp.bottomMargin); childHeightMeasureSpec = MeasureSpec.makeMeasureSpec( height,1)"> { childHeightMeasureSpec = getChildMeasureSpec(heightMeasureSpec,getPaddingTopWithForeground() + getPaddingBottomWithForeground() + lp.topMargin + lp.bottomMargin,lp.height); } child.measure(childWidthMeasureSpec,childHeightMeasureSpec); } } }

FrameLayout 是 ViewGroup 的子类,后者有一个 View[] 类型的成员变量 mChildren,代表了其子 View 集合。通过 getChildAt(i) 能获取指定索引处的子 View,通过 getChildCount() 可以获得子 View 的总数。

在上面的源码中,首先调用 measureChildWithMargins() 方法对所有子 View 进行了一遍测量,并计算出所有子 View 的最大宽度和最大高度。而后将得到的最大高度和宽度加上padding,这里的 padding 包括了父 View 的 padding 和前景区域的 padding。然后会检查是否设置了最小宽高,并与其比较,将两者中较大的设为最终的最大宽高。最后,若设置了前景图像,我们还要检查前景图像的最小宽高。

经过了以上一系列步骤后,我们就得到了 maxHeight 和 maxWidth 的最终值,表示当前容器 View 用这个尺寸就能够正常显示其所有子 View(同时考虑了 padding 和 margin )。而后我们需要调用 resolveSizeAndState() 方法来结合传来的 MeasureSpec 来获取最终的测量宽高,并保存到 mMeasuredWidth 与 mMeasuredHeight 成员变量中。

如果存在一些子 View 的宽或高是 MATCH_PARENT,那么需要等父 View 的尺寸计算出来后,重新计算这些子 View 的 MeasureSpec,再来测量这些子 view 的宽高。

这里提醒我们在自定义 View 的时候需要考虑你的子 View 是不是和你自定义的 View 的大小是一样,如果一样,就需要等自定义 View 的大小确定了,再重新测量一遍。

下面看看 measureChildWithMargins() 方法具体逻辑:


     * Ask one of the children of this view to measure itself,taking into
     * account both the MeasureSpec requirements for this view and its padding
     * and margins. The child must have MarginLayoutParams The heavy lifting is
     * done in getChildMeasureSpec.
     *
     * @param child The child to measure
     *  parentWidthMeasureSpec The width requirements for this view
     *  widthUsed Extra space that has been used up by the parent
     *        horizontally (possibly by other children of the parent)
     *  parentHeightMeasureSpec The height requirements for this view
     *  heightUsed Extra space that has been used up by the parent
     *        vertically (possibly by other children of the parent)
     */
     measureChildWithMargins(View child,1)">int parentWidthMeasureSpec,1)"> widthUsed,1)">int parentHeightMeasureSpec,1)"> heightUsed) {
         (MarginLayoutParams) child.getLayoutParams();

        int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,mPaddingLeft + mPaddingRight + lp.leftMargin + lp.rightMargin
                        +int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,mPaddingTop + mPaddingBottom + lp.topMargin + lp.bottomMargin
                        + heightUsed,lp.height);

        child.measure(childWidthMeasureSpec,childHeightMeasureSpec);
    }

 该方法主要是获取子 View 的 MeasureSpec,然后调用 child.measure() 来完成子 View 的测量。下面看看子 View 获取 MeasureSpec 的具体逻辑:

 int getChildMeasureSpec(int spec,1)">int padding,1)"> childDimension) {
// 父 view 的 mode 和 size int specMode = MeasureSpec.getMode(spec); int specsize = MeasureSpec.getSize(spec);      // 去掉 padding int size = Math.max(0,specsize - padding); int resultSize = 0int resultMode = 0 (specMode) { Parent has imposed an exact size on us MeasureSpec.EXACTLY: if (childDimension >= 0) { resultSize = childDimension; resultMode = MeasureSpec.EXACTLY; } else if (childDimension == LayoutParams.MATCH_PARENT) { Child wants to be our size. So be it. resultSize = size; resultMode = LayoutParams.WRAP_CONTENT) { Child wants to determine its own size. It can''t be bigger than us. resultSize = MeasureSpec.AT_MOST; } Parent has imposed a maximum size on us MeasureSpec.AT_MOST: Child wants a specific size... so be it resultSize = Child wants to be our size,but our size is not fixed. Constrain child to not be bigger than us. resultSize = MeasureSpec.AT_MOST; } Parent asked to see how big we want to be MeasureSpec.UNSPECIFIED: Child wants a specific size... let him have it resultSize = Child wants to be our size... find out how big it should be resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 : size; resultMode = MeasureSpec.UNSPECIFIED; } Child wants to determine its own size.... find out how big it should be resultSize = View.sUseZeroUnspecifiedMeasureSpec ? 0 MeasureSpec.UNSPECIFIED; } noinspection ResourceType MeasureSpec.makeMeasureSpec(resultSize,resultMode); }

 方法清楚展示了普通 View 的 MeasureSpec 的创建规则,每个 View 的 MeasureSpec 状态量由其直接父 View 的 MeasureSpec 和 View 自身的属性 LayoutParams (LayoutParams 有宽高尺寸值等信息)共同决定。

从上面的代码可以知道,返回 View 的 MeasureSpec 大致可以分为一下机制情况:
  • 子 View 为具体的宽/高,那么 View 的 MeasureSpec 都为 LayoutParams 中大小。

  • 子 View 为 match_parent,父元素为精度模式(EXACTLY),那么 View 的 MeasureSpec 也是精准模式他的大小不会超过父容器的剩余空间。

  • 子 View 为 wrap_content,不管父元素是精准模式还是最大化模式(AT_MOST),View 的 MeasureSpec 总是为最大化模式并且大小不超过父容器的剩余空间。

  • 父容器为 UNSPECIFIED 模式主要用于系统多次 Measure 的情形,一般我们不需要关心。

总结为下表:

 View.measure()  代码逻辑前面已经分析过了,最终会调用 onMeasuere 方法,下面看下 View.onMeasuere() 的代码:

 protected int heightMeasureSpec) {
        setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(),widthMeasureSpec),getDefaultSize(getSuggestedMinimumHeight(),heightMeasureSpec));
    }

上面方法中调用了 方法中调用了 setMeasuredDimension()方法,setMeasuredDimension()又调用了 getDefaultSize() 方法。getDefaultSize() 又调用了getSuggestedMinimumWidth()和 getSuggestedMinimumHeight(),那反向研究一下,先看下 getSuggestedMinimumWidth() 方法  (getSuggestedMinimumHeight() 原理 getSuggestedMinimumWidth() 跟一样)。 

  getSuggestedMinimumWidth() {
        return (mBackground == null) ? mMinWidth : max(mMinWidth,mBackground.getMinimumWidth());
    }

 源码很简单,如果 View 没有背景,就直接返回 View 本身的最小宽度 mMinWidth;如果给 View 设置了背景,就取 View 本身的最小宽度 mMinWidth 和背景的最小宽度的最大值.

那么 mMinWidth 是哪里来的?搜索下源码就可以知道,View 的最小宽度 mMinWidth 可以有两种方式进行设置:

  • 第一种是在 View 的构造方法中进行赋值的,View 通过读取 XML 文件中View设置的 minWidth 属性来为 mMinWidth 赋值
 R.styleable.View_minWidth:
     mMinWidth = a.getDimensionPixelSize(attr,1)">);
     break;
  •  第二种是在调用 View 的 setMinimumWidth 方法为 mMinWidth 赋值
void setMinimumWidth( minWidth) {
    mMinWidth = minWidth;
    requestLayout();
}

 下面看下 getDefaultSize() 的代码逻辑:

    int getDefaultSize(int size,1)"> measureSpec) {
        int result = size;
         MeasureSpec.getMode(measureSpec);
         MeasureSpec.getSize(measureSpec);

         MeasureSpec.UNSPECIFIED:
            result = size;
             MeasureSpec.AT_MOST:
         MeasureSpec.EXACTLY:
            result = specsize;
             result;
    }

从注释可以看出,getDefaultSize()这个测量方法并没有适配 wrap_content 这一种布局模式,只是简单地将 wrap_content 跟 match_parent 等同起来。

到了这里,我们要注意一个问题:

getDefaultSize()方法中 wrap_content 和 match_parent 属性的效果是一样的,而该方法是 View 的 onMeasure()中默认调用的,也就是说,对于一个直接继承自 View 的自定义 View 来说,它的 wrap_content 和 match_parent 属性是一样的效果,因此如果要实现自定义 View 的 wrap_content,则要重写 onMeasure() 方法,对 wrap_content 属性进行处理。

如何处理呢?也很简单,代码如下所示:

protected void onMeasure(int widthMeasureSpec,1)"> heightMeasureSpec){
  super.onMeasure(widthMeasureSpec, heightMeasureSpec);
  取得父ViewGroup指定的宽高测量模式和尺寸
  int widthSpecMode = MeasureSpec.getMode(widthMeasureSpec);
  int widthSpecsize = MeasureSpec.getSize(widthMeasureSpec);
  int heightSpecMode = MeasureSpec.getMode(heightMeasureSpec);
  int heightSpecsize = MeasureSpec.getSize(heightMeasureSpec);
  if (widthSpecMode == MeasureSpec.AT_MOST && heightSpecMode == MeasureSpec.AT_MOST) {
    如果宽高都是AT_MOST的话,即都是wrap_content布局模式,就用View自己想要的宽高值
        setMeasuredDimension(mWidth, mHeight);
  }else if (widthSpecMode ==如果只有宽度都是AT_MOST的话,即只有宽度是wrap_content布局模式,宽度就用View自己想要的宽度值,高度就用父ViewGroup指定的高度值
pecsize);
  }if (heightSpecMode ==如果只有高度都是AT_MOST的话,即只有高度是wrap_content布局模式,高度就用View自己想要的宽度值,宽度就用父ViewGroup指定的高度值
        setMeasuredDimension(widthSpecsize, mHeight);
  }
}

 在上面的代码中,我们要给 View 指定一个默认的内部宽/高(mWidth 和 mHeight),并在 wrap_content 时设置此宽/高即可。最后将在将宽高设置到 View 上:

    // View    
    protected void setMeasuredDimension(int measuredWidth,1)"> measuredHeight) {
        boolean optical = isLayoutModeOptical(this);
        if (optical != isLayoutModeOptical(mParent)) {
            Insets insets = getopticalInsets();
            int opticalWidth  = insets.left + insets.right;
            int opticalHeight = insets.top  + insets.bottom;
 
            measuredWidth  += optical ? opticalWidth  : -opticalWidth;
            measuredHeight += optical ? opticalHeight : -opticalHeight;
        }
        setMeasuredDimensionRaw(measuredWidth, measuredHeight);
    }

    void setMeasuredDimensionRaw( measuredHeight) {
        mMeasuredWidth = measuredWidth;
        mMeasuredHeight = measuredHeight;
        mPrivateFlags |= PFLAG_MEASURED_DIMENSION_SET;
    }

这里就是把测量完的宽高值赋值给 mMeasuredWidthmMeasuredHeight 这两个 View 的属性,然后将标志位置为已测量状态。

子 View 测量完成以后,会计算 childState,看下 combineMeasuredStates 方法 :

int combineMeasuredStates(int curState,1)"> newState) {
        return curState | newState;
    }

 当前 curState 为 0, newState 是调用 child.getMeasuredState() 方法得到的,来看下这个方法的具体逻辑:

 
     * Return only the state bits of {@link #getMeasuredWidthAndState()}
     * and { #getMeasuredHeightAndState()},combined into one integer.
     * The width component is in the regular bits { #MEASURED_STATE_MASK}
     * and the height component is at the shifted bits
     * { #MEASURED_HEIGHT_STATE_SHIFT}>>{ #MEASURED_STATE_MASK}.
      getMeasuredState() {
        return (mMeasuredWidth&MEASURED_STATE_MASK)
                | ((mMeasuredHeight>>MEASURED_HEIGHT_STATE_SHIFT)
                        & (MEASURED_STATE_MASK>>MEASURED_HEIGHT_STATE_SHIFT));
    }

 该方法返回一个 int 值,该值同时包含宽度的 state 以及高度的 state 信息,不包含任何的尺寸信息。

  • MEASURED_STATE_MASK 的值为 0xff000000,其高字节的 8 位全部为 1,低字节的 24 位全部为 0。

  • MEASURED_HEIGHT_STATE_SHIFT 值为 16。

  • MEASURED_STATE_MASK 与 mMeasuredWidth 做与操作之后就取出了存储在宽度首字节中的 state 信息,过滤掉低位三个字节的尺寸信息。

  • 由于 int 有四个字节,首字节已经存了宽度的 state 信息,那么高度的 state 信息就不能存在首位字节。MEASURED_STATE_MASK 向右移 16 位,变成了 0x0000ff00,这个值与高度值 mMeasuredHeight 做与操作就取出了 mMeasuredHeight 第三个字节中的信息。而 mMeasuredHeight 的 state 信息是存在首字节中,所以也得对mMeasuredHeight 向右移相同的位置,这样就把 state 信息移到了第三个字节中。

  • 最后,将得到的宽度 state 与高度 state 按位或操作,这样就拼接成一个 int 值,该值首个字节存储宽度的 state 信息,第三个字节存储高度的 state 信息。

这些都得到之后,就可以开始去计算父 View 的尺寸了:

        // 确定父 View 的宽高
        setMeasuredDimension(resolveSizeAndState(maxWidth,1)">resolveSizeAndState(maxHeight,childState << MEASURED_HEIGHT_STATE_SHIFT));

 下面开始看 resolveSizeAndState 具体逻辑:

 View 的静态方法
int resolveSizeAndState(int measureSpec,1)"> childMeasuredState) {
         MeasureSpec.getSize(measureSpec);
         result;
         (specMode) {
             MeasureSpec.AT_MOST:
                if (specsize < size) {
                    result = specsize | MEASURED_STATE_TOO_SMALL;
                }  {
                    result = size;
                }
                 MeasureSpec.EXACTLY:
                result = specsize;
                :
                result = size;
        }
        return result | (childMeasuredState & MEASURED_STATE_MASK);
    }

这个方法的代码结构跟前文提到的 getDefaultSize()方法很相似,主要的区别在于 specMode 为 AT_MOST 的情况。我们当时说 getDefaultSize() 方法是没有适配wrap_content 这种情况,而这个 resolveSizeAndState() 方法是已经适配了 wrap_content 的布局方式,那具体怎么实现 AT_MOST 测量逻辑的呢?有两种情况:

  • 当父 ViewGroup 指定的最大尺寸比 View 想要的尺寸还要小时,会给这个父 ViewGroup  的指定的最大值 specsize 加入一个尺寸太小的标志  MEASURED_STATE_TOO_SMALL,然后将这个带有标志的尺寸返回,父 ViewGroup 通过该标志就可以知道分配给 View 的空间太小了,在窗口协商测量的时候会根据这个标志位来做窗口大小的决策。

  • 当父 ViewGroup 指定的最大尺寸比没有比 View 想要的尺寸小时(相等或者 View 想要的尺寸更小),直接取 View 想要的尺寸,然后返回该尺寸。

getDefaultSize() 方法只是 onMeasure() 方法中获取最终尺寸的默认实现,其返回的信息比 resolveSizeAndState() 要少,那么什么时候才会调用 resolveSizeAndState() 方法呢? 主要有两种情况:

  • Android 中的大部分 ViewGroup 类都调用了 resolveSizeAndState() 方法,比如 LinearLayout 在测量过程中会调用 resolveSizeAndState() 方法而非 getDefaultSize()方法。

  • 我们自己在实现自定义的 View 或 ViewGroup 时,我们可以重写 onMeasure() 方法,并在该方法内调用 resolveSizeAndState() 方法。

到此,终于把 View 测量过程讲完了。

下一篇开始讲 View 的 layout 和 draw 过程。

 

参考文章

Android源码完全解析——View的Measure过程

View的绘制流程 

总结

以上是小编为你收集整理的Android View 的绘制流程之 Measure 过程详解 (一) Android View 绘制流程之 DecorView 与 ViewRootImplAndroid View 的绘制流程之 Measure 过程详解 (一)Android View 的绘制流程之 Layout 和 Draw 过程详解 (二)Android View 的事件分发原理解析Android 自定义 View 详解Android View 绘制流程之 DecorView 与 ViewRootImp全部内容。

如果觉得小编网站内容还不错,欢迎将小编网站推荐给好友。

Android View 绘制流程之 DecorView 与 ViewRootImpl

Android View 绘制流程之 DecorView 与 ViewRootImpl

一年多以前,曾经以为自己对 View 的添加显示逻辑已经有所了解了,事后发现也只是懂了些皮毛而已。经过一年多的实战,Android 和 Java 基础都有了提升,是时候该去看看 DecorView 的添加显示。

View 的绘制系列文章:

  • Android View 绘制流程之 DecorView 与 ViewRootImpl

  • Android View 的绘制流程之 Measure 过程详解 (一)

  • Android View 的绘制流程之 Layout 和 Draw 过程详解 (二)

  • Android View 的事件分发原理解析

  • Android 自定义 View 详解

概论

Android 中 Activity 是作为应用程序的载体存在,代表着一个完整的用户界面,提供了一个窗口来绘制各种视图,当 Activity 启动时,我们会通过 setContentView 方法来设置一个内容视图,这个内容视图就是用户看到的界面。那么 View 和 activity 是如何关联在一起的呢 ?

 上图是 View 和 Activity 之间的关系。先解释图中一些类的作用以及相关关系:

  • Activity : 对于每一个 activity 都会有拥有一个 PhoneWindow。

  • PhoneWindow :该类继承于 Window 类,是 Window 类的具体实现,即我们可以通过该类具体去绘制窗口。并且,该类内部包含了一个 DecorView 对象,该 DectorView 对象是所有应用窗口的根 View。
  • DecorView 是一个应用窗口的根容器,它本质上是一个 FrameLayout。DecorView 有唯一一个子 View,它是一个垂直 LinearLayout,包含两个子元素,一个是 TitleView( ActionBar 的容器),另一个是 ContentView(窗口内容的容器)。

  • ContentView :是一个 FrameLayout(android.R.id.content),我们平常用的 setContentView 就是设置它的子 View 。

  • WindowManager : 是一个接口,里面常用的方法有:添加 View,更新 View 和删除 View。主要是用来管理 Window 的。WindowManager 具体的实现类是 WindowManagerImpl。最终,WindowManagerImpl 会将业务交给 WindowManagerGlobal 来处理。
  • WindowManagerService (WMS) : 负责管理各 app 窗口的创建,更新,删除, 显示顺序。运行在 system_server 进程。

ViewRootImpl :拥有 DecorView 的实例,通过该实例来控制 DecorView 绘制。ViewRootImpl 的一个内部类 W,实现了 IWindow 接口,IWindow 接口是供 WMS 使用的,WSM 通过调用 IWindow 一些方法,通过 Binder 通信的方式,最后执行到了 W 中对应的方法中。同样的,ViewRootImpl 通过 IWindowSession 来调用 WMS 的 Session 一些方法。Session 类继承自 IWindowSession.Stub,每一个应用进程都有一个唯一的 Session 对象与 WMS 通信。

DecorView 的创建 

先从 Mainactivity 中的代码看起,首先是调用了 setContentView;

protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);
}

该方法是父类 AppCompatActivity 的方法,最终会调用 AppCompatDelegateImpl 的 setContentView 方法:

// AppCompatDelegateImpl  
public void setContentView(int resId) { this.ensureSubDecor(); ViewGroup contentParent = (ViewGroup)this.mSubDecor.findViewById(16908290); contentParent.removeAllViews(); LayoutInflater.from(this.mContext).inflate(resId, contentParent); this.mOriginalWindowCallback.onContentChanged(); }

ensureSubDecor 从字面理解就是创建 subDecorView,这个是根据主题来创建的,下文也会讲到。创建完以后,从中获取 contentParent,再将从 activity 传入的 id xml 布局添加到里面。不过大家注意的是,在添加之前先调用 removeAllViews () 方法,确保没有其他子 View 的干扰。

private void ensureSubDecor() {
        if (!this.mSubDecorInstalled) {
            this.mSubDecor = this.createSubDecor(); 
            ......
        }
        ......
    }        

 最终会调用 createSubDecor () ,来看看里面的具体代码逻辑:

private ViewGroup createSubDecor() {
        // 1、获取主题参数,进行一些设置,包括标题,actionbar 等 
        TypedArray a = this.mContext.obtainStyledAttributes(styleable.AppCompatTheme);
        if (!a.hasValue(styleable.AppCompatTheme_windowActionBar)) {
            a.recycle();
            throw new IllegalStateException("You need to use a Theme.AppCompat theme (or descendant) with this activity.");
        } else {
            if (a.getBoolean(styleable.AppCompatTheme_windowNoTitle, false)) {
                this.requestWindowFeature(1);
            } else if (a.getBoolean(styleable.AppCompatTheme_windowActionBar, false)) {
                this.requestWindowFeature(108);
            }

            if (a.getBoolean(styleable.AppCompatTheme_windowActionBarOverlay, false)) {
                this.requestWindowFeature(109);
            }

            if (a.getBoolean(styleable.AppCompatTheme_windowActionModeOverlay, false)) {
                this.requestWindowFeature(10);
            }

            this.mIsFloating = a.getBoolean(styleable.AppCompatTheme_android_windowIsFloating, false);
            a.recycle();
            // 2、确保优先初始化 DecorView
            this.mWindow.getDecorView();
            LayoutInflater inflater = LayoutInflater.from(this.mContext);
            ViewGroup subDecor = null;
            // 3、根据不同的设置来对 subDecor 进行初始化
            if (!this.mWindowNoTitle) {
                if (this.mIsFloating) {
                    subDecor = (ViewGroup)inflater.inflate(layout.abc_dialog_title_material, (ViewGroup)null);
                    this.mHasActionBar = this.mOverlayActionBar = false;
                } else if (this.mHasActionBar) {
                    TypedValue outValue = new TypedValue();
                    this.mContext.getTheme().resolveAttribute(attr.actionBarTheme, outValue, true);
                    Object themedContext;
                    if (outValue.resourceId != 0) {
                        themedContext = new ContextThemeWrapper(this.mContext, outValue.resourceId);
                    } else {
                        themedContext = this.mContext;
                    }

                    subDecor = (ViewGroup)LayoutInflater.from((Context)themedContext).inflate(layout.abc_screen_toolbar, (ViewGroup)null);
                    this.mDecorContentParent = (DecorContentParent)subDecor.findViewById(id.decor_content_parent);
                    this.mDecorContentParent.setWindowCallback(this.getWindowCallback());
                    if (this.mOverlayActionBar) {
                        this.mDecorContentParent.initFeature(109);
                    }

                    if (this.mFeatureProgress) {
                        this.mDecorContentParent.initFeature(2);
                    }

                    if (this.mFeatureIndeterminateProgress) {
                        this.mDecorContentParent.initFeature(5);
                    }
                }
            } else {
                if (this.mOverlayActionMode) {
                    subDecor = (ViewGroup)inflater.inflate(layout.abc_screen_simple_overlay_action_mode, (ViewGroup)null);
                } else {
                    subDecor = (ViewGroup)inflater.inflate(layout.abc_screen_simple, (ViewGroup)null);
                }

                if (VERSION.SDK_INT >= 21) {
                    ViewCompat.setOnApplyWindowInsetsListener(subDecor, new OnApplyWindowInsetsListener() {
                        public WindowInsetsCompat onApplyWindowInsets(View v, WindowInsetsCompat insets) {
                            int top = insets.getSystemWindowInsetTop();
                            int newTop = AppCompatDelegateImpl.this.updateStatusGuard(top);
                            if (top != newTop) {
                                insets = insets.replaceSystemWindowInsets(insets.getSystemWindowInsetLeft(), newTop, insets.getSystemWindowInsetRight(), insets.getSystemWindowInsetBottom());
                            }

                            return ViewCompat.onApplyWindowInsets(v, insets);
                        }
                    });
                } else {
                    ((FitWindowsViewGroup)subDecor).setOnFitSystemWindowsListener(new OnFitSystemWindowsListener() {
                        public void onFitSystemWindows(Rect insets) {
                            insets.top = AppCompatDelegateImpl.this.updateStatusGuard(insets.top);
                        }
                    });
                }
            }

            if (subDecor == null) {
                throw new IllegalArgumentException("AppCompat does not support the current theme features: { windowActionBar: " + this.mHasActionBar + ", windowActionBarOverlay: " + this.mOverlayActionBar + ", android:windowIsFloating: " + this.mIsFloating + ", windowActionModeOverlay: " + this.mOverlayActionMode + ", windowNoTitle: " + this.mWindowNoTitle + " }");
            } else {
                if (this.mDecorContentParent == null) {
                    this.mTitleView = (TextView)subDecor.findViewById(id.title);
                }

                ViewUtils.makeOptionalFitsSystemWindows(subDecor);
                ContentFrameLayout contentView = (ContentFrameLayout)subDecor.findViewById(id.action_bar_activity_content);
                ViewGroup windowContentView = (ViewGroup)this.mWindow.findViewById(16908290);
                if (windowContentView != null) {
                    while(windowContentView.getChildCount() > 0) {
                        View child = windowContentView.getChildAt(0);
                        windowContentView.removeViewAt(0);
                        contentView.addView(child);
                    }

                    windowContentView.setId(-1);
                    contentView.setId(16908290);
                    if (windowContentView instanceof FrameLayout) {
                        ((FrameLayout)windowContentView).setForeground((Drawable)null);
                    }
                }
                // 将 subDecor 添加到 DecorView 中
                this.mWindow.setContentView(subDecor);
                contentView.setAttachListener(new OnAttachListener() {
                    public void onAttachedFromWindow() {
                    }

                    public void onDetachedFromWindow() {
                        AppCompatDelegateImpl.this.dismissPopups();
                    }
                });
                return subDecor;
            }
        }
    }
                    

上面的代码总结来说就是在做一件事,就是创建 subDecor。摊开来说具体如下:

1、根据用户选择的主题来设置一些显示特性,包括标题,actionbar 等。

2、根据不同特性来初始化 subDecor;对 subDecor 内部的子 View 进行初始化。

3、最后添加到 DecorView 中。

添加的具体代码如下:此处是通过调用 

  // AppCompatDelegateImpl
  this.mWindow.getDecorView();

   // phoneWindow 
   public final View getDecorView() {
        if (mDecor == null || mForceDecorInstall) {
            installDecor();
        }
        return mDecor;
    }
 

private void installDecor() {
        mForceDecorInstall = false;
        if (mDecor == null) {
            // 生成 DecorView
            mDecor = generateDecor(-1);
            mDecor.setDescendantFocusability(ViewGroup.FOCUS_AFTER_DESCENDANTS);
            mDecor.setIsRootNamespace(true);
            if (!mInvalidatePanelMenuPosted && mInvalidatePanelMenuFeatures != 0) {
                mDecor.postOnAnimation(mInvalidatePanelMenuRunnable);
            }
        } else {
            // 这样 DecorView 就持有了window
            mDecor.setWindow(this);
        }
      ......
}


   protected DecorView generateDecor(int featureId) {
        // System process doesn''t have application context and in that case we need to directly use
        // the context we have. Otherwise we want the application context, so we don''t cling to the
        // activity.
        Context context;
        if (mUseDecorContext) {
            Context applicationContext = getContext().getApplicationContext();
            if (applicationContext == null) {
                context = getContext();
            } else {
                context = new DecorContext(applicationContext, getContext());
                if (mTheme != -1) {
                    context.setTheme(mTheme);
                }
            }
        } else {
            context = getContext();
        }
        return new DecorView(context, featureId, this, getAttributes());
   }

到此,DecorView 的创建就讲完了。可是我们似乎并没有看到 DecorView 是被添加的,什么时候对用户可见的。

 WindowManager

View 创建完以后,那 Decorview 是怎么添加到屏幕中去的呢?当然是 WindowManager 呢,那么是如何将 View 传到 WindowManager 中呢。

看 ActivityThread 中的 handleResumeActivity 方法:

// ActivityThread
public
void handleResumeActivity(IBinder token, boolean finalStateRequest, boolean isForward, String reason) { ...... final int forwardBit = isForward ? WindowManager.LayoutParams.SOFT_INPUT_IS_FORWARD_NAVIGATION : 0; // If the window hasn''t yet been added to the window manager, // and this guy didn''t finish itself or start another activity, // then go ahead and add the window. boolean willBeVisible = !a.mStartedActivity; if (!willBeVisible) { try { willBeVisible = ActivityManager.getService().willActivityBeVisible( a.getActivityToken()); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); } } 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) { if (!a.mWindowAdded) { a.mWindowAdded = true; wm.addView(decor, l); } else { // The activity will get a callback for this {@link LayoutParams} change // earlier. However, at that time the decor will not be set (this is set // in this method), so no action will be taken. This call ensures the // callback occurs with the decor set. a.onWindowAttributesChanged(l); } } // If the window has already been added, but during resume // we started another activity, then don''t yet make the // window visible. } else if (!willBeVisible) { if (localLOGV) Slog.v(TAG, "Launch " + r + " mStartedActivity set"); r.hideForNow = true; } // Get rid of anything left hanging around. cleanUpPendingRemoveWindows(r, false /* force */); // The window is now visible if it has been added, we are not // simply finishing, and we are not starting another activity. if (!r.activity.mFinished && willBeVisible && r.activity.mDecor != null && !r.hideForNow) { if (r.newConfig != null) { performConfigurationChangedForActivity(r, r.newConfig); if (DEBUG_CONFIGURATION) { Slog.v(TAG, "Resuming activity " + r.activityInfo.name + " with newConfig " + r.activity.mCurrentConfig); } r.newConfig = null; } if (localLOGV) Slog.v(TAG, "Resuming " + r + " with isForward=" + isForward); WindowManager.LayoutParams l = r.window.getAttributes(); if ((l.softInputMode & WindowManager.LayoutParams.SOFT_INPUT_IS_FORWARD_NAVIGATION) != forwardBit) { l.softInputMode = (l.softInputMode & (~WindowManager.LayoutParams.SOFT_INPUT_IS_FORWARD_NAVIGATION)) | forwardBit; if (r.activity.mVisibleFromClient) { ViewManager wm = a.getWindowManager(); View decor = r.window.getDecorView(); wm.updateViewLayout(decor, l); } } r.activity.mVisibleFromServer = true; mNumVisibleActivities++; if (r.activity.mVisibleFromClient) {           // 这里也会调用addview r.activity.makeVisible(); } } r.nextIdle = mNewActivities; mNewActivities = r; if (localLOGV) Slog.v(TAG, "Scheduling idle handler for " + r); Looper.myQueue().addIdleHandler(new Idler()); }

上面的代码主要做了以下几件事:

1、获取到 DecorView,设置不可见,然后通过 wm.addView (decor, l) 将 view 添加到 WindowManager;

2、在某些情况下,比如此时点击了输入框调起了键盘,就会调用 wm.updateViewLayout (decor, l) 来更新 View 的布局。

3、这些做完以后,会调用 activity 的  makeVisible ,让视图可见。如果此时 DecorView 没有添加到 WindowManager,那么会添加。 

// Activity
void makeVisible() { if (!mWindowAdded) { ViewManager wm = getWindowManager(); wm.addView(mDecor, getWindow().getAttributes()); mWindowAdded = true; } mDecor.setVisibility(View.VISIBLE); }

 接下来,看下 addview 的逻辑。 WindowManager 的实现类是 WindowManagerImpl,而它则是通过 WindowManagerGlobal 代理实现 addView 的,我们看下 addView 的方法:

// WindowManagerGlobal  
 public void addView(View view, ViewGroup.LayoutParams params,
            Display display, Window parentWindow) {
           // ......
    
            root = new ViewRootImpl(view.getContext(), display);
            view.setLayoutParams(wparams);

            mViews.add(view);
            mRoots.add(root);
            mParams.add(wparams);
           // do this last because it fires off messages to start doing things
            try {
                root.setView(view, wparams, panelParentView);
            } catch (RuntimeException e) {
                // BadTokenException or InvalidDisplayException, clean up.
                if (index >= 0) {
                    removeViewLocked(index, true);
                }
                throw e;
            } 
}

在这里,实例化了 ViewRootImpl 。同时调用 ViewRootImpl 的 setView 方法来持有了 DecorView。此外这里还保存了 DecorView ,Params,以及 ViewRootImpl 的实例。

现在我们终于知道为啥 View 是在 OnResume 的时候可见的呢。

 ViewRootImpl

实际上,View 的绘制是由 ViewRootImpl 来负责的。每个应用程序窗口的 DecorView 都有一个与之关联的 ViewRootImpl 对象,这种关联关系是由 WindowManager 来维护的。

先看 ViewRootImpl 的 setView 方法,该方法很长,我们将一些不重要的点注释掉:

/**
     * We have one child
     */
    public void setView(View view, WindowManager.LayoutParams attrs, View panelParentView) {
        synchronized (this) {
            if (mView == null) {
                mView = view;
                ......
               
                mAdded = true;
                int res; /* = WindowManagerImpl.ADD_OKAY; */

                // 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();
                ......
            }
        }
    }

这里先将 mView 保存了 DecorView 的实例,然后调用 requestLayout () 方法,以完成应用程序用户界面的初次布局。

public void requestLayout() {
        if (!mHandlingLayoutInLayoutRequest) {
            checkThread();
            mLayoutRequested = true;
            scheduleTraversals();
        }
    }

因为是 UI 绘制,所以一定要确保是在主线程进行的,checkThread 主要是做一个校验。接着调用 scheduleTraversals 开始计划绘制了。

void scheduleTraversals() {
        if (!mTraversalScheduled) {
            mTraversalScheduled = true;
            mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier();
            mChoreographer.postCallback(
                    Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
            if (!mUnbufferedInputDispatch) {
                scheduleConsumeBatchedInput();
            }
            notifyRendererOfFramePending();
            pokeDrawLockIfNeeded();
        }
    }

这里主要关注两点:

mTraversalBarrier : Handler 的同步屏障。它的作用是可以拦截 Looper 对同步消息的获取和分发,加入同步屏障之后,Looper 只会获取和处理异步消息,如果没有异步消息那么就会进入阻塞状态。也就是说,对 View 绘制渲染的处理操作可以优先处理(设置为异步消息)。

mChoreographer: 编舞者。统一动画、输入和绘制时机。也是这章需要重点分析的内容。

mTraversalRunnable :TraversalRunnable 的实例,是一个 Runnable,最终肯定会调用其 run 方法:

final class TraversalRunnable implements Runnable {
        @Override
        public void run() {
            doTraversal();
        }
    }

doTraversal,如其名,开始绘制了,该方法内部最终会调用 performTraversals 进行绘制。

void doTraversal() {
        if (mTraversalScheduled) {
            mTraversalScheduled = false;
            mHandler.getLooper().getQueue().removeSyncBarrier(mTraversalBarrier);

            if (mProfile) {
                Debug.startMethodTracing("ViewAncestor");
            }

            performTraversals();

            if (mProfile) {
                Debug.stopMethodTracing();
                mProfile = false;
            }
        }
    }

到此,DecorView 与 activity 之间的绑定关系就讲完了,下一章,将会介绍 performTraversals 所做的事情,也就是 View 绘制流程。 

附上一张流程图:

 

到此,DecorView 与 ViewRootImpl 之间的关系就讲的很清楚了。

点击查看下一篇 Android View 的测量流程详解

原文出处:https://www.cnblogs.com/huansky/p/11911549.html

关于Android-ViewRootImpl $ CalledFromWrongThreadException的问题就给大家分享到这里,感谢你花时间阅读本站内容,更多关于Android android.view.ViewRoot$CalledFromWrongThreadException异常的解决方案、Android DecorView与ViewRootImpl、Android View 的绘制流程之 Measure 过程详解 (一) Android View 绘制流程之 DecorView 与 ViewRootImplAndroid View 的绘制流程之 Measure 过程详解 (一)Android View 的绘制流程之 Layout 和 Draw 过程详解 (二)Android View 的事件分发原理解析Android 自定义 View 详解Android View 绘制流程之 DecorView 与 ViewRootImp、Android View 绘制流程之 DecorView 与 ViewRootImpl等相关知识的信息别忘了在本站进行查找喔。

本文标签: