欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  科技

Android自定义View5--getWidth()和getMeasuredWidth()区别, view.post(Runnable)引发内存泄漏的原因和解决

程序员文章站 2022-03-10 23:44:03
getMeasuredHeight()返回的是原始测量高度,与屏幕无关getHeight()返回的是在屏幕上显示的高度实际上在当屏幕可以包裹内容的时候,他们的值是相等的,只有当view超出屏幕后,才能看出他们的区别。当超出屏幕后,getMeasuredHeight()等于getHeight()加上屏幕之外没有显示的高度。在自定义控件时,调用子View的getHeight()得到的结果为0,调用getMeasuredHeight()为子View的真实大小。view.post(new Runnable...

参考文章:
https://blog.csdn.net/carson_ho
http://blog.csdn.net/a740169405/article/details/69668957
https://mp.weixin.qq.com/s/laR3_Xvr0Slb4oR8D1eQyQ?
https://blog.csdn.net/ChrisSen/article/details/84065243
https://cloud.tencent.com/developer/article/1572159
https://blog.csdn.net/u013750244/article/details/108129941

一、 getMeasuredWidth() / getMeasuredHeight()返回值

1.1 结论

返回的值是 View在Measure过程中测量的宽 / 高

1.2 源码分析
1、由于getMeasuredWidth()与getMeasuredHeight()同理,下面只讲解getMeasuredWidth()
2、请务必先了解自定义View的Measure过程: 自定义View Measure过程 - 最易懂的自定义View原理系列(2)

public final int getMeasuredWidth() {  
      return mMeasuredWidth & MEASURED_SIZE_MASK;  
      // getMeasuredWidth()的返回值是mMeasuredWidth(MEASURED_SIZE_MASK = 静态常量 = 限制mMeasuredWidth大小)
      // 该值的赋值来源:Measure过程中的setMeasuredDimension() -> 分析1
      //
  }  

/**
  * 分析1:setMeasuredDimension()
  * 作用:存储测量后的View宽 / 高
  * 注:该方法即为我们重写onMeasure()所要实现的最终目的
  **/
    protected final void setMeasuredDimension(int measuredWidth, int measuredHeight) {  
    //参数说明:测量后子View的宽 / 高值

        // 特别注意:
        // 将测量后子View的宽 / 高值进行传递
        // 正是这里,赋值mMeasuredWidth的 = measuredWidth
            mMeasuredWidth = measuredWidth;
            mMeasuredHeight = measuredHeight;  
          
            mPrivateFlags |= PFLAG_MEASURED_DIMENSION_SET;  
        } 
    // setMeasuredDimension()的参数measuredWidth 是从getDefaultSize()获得的 
    // 在onMeasure()里调用 -> 分析2

/**
  * 分析2:onMeasure()
  * 作用:a. 根据View宽/高的测量规格计算View的宽/高值:getDefaultSize()
  *      b. 存储测量后的View宽 / 高:setMeasuredDimension()
  **/ 
  protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {  
    // 参数说明:View的宽 / 高测量规格

    // 特别注意,正是这句话,下面我们继续看getDefaultSize()的介绍 -> 分析3
    setMeasuredDimension(getDefaultSize(getSuggestedMinimumWidth(), widthMeasureSpec),  
                         getDefaultSize(getSuggestedMinimumHeight(), heightMeasureSpec));  
}

/**
  * 分析3:getDefaultSize()
  * 作用:根据View宽/高的测量规格计算View的宽/高值
  **/
  public static int getDefaultSize(int size, int measureSpec) {  

        // 参数说明:
        // size:提供的默认大小
        // measureSpec:宽/高的测量规格(含模式 & 测量大小)

            // 设置默认大小
            int result = size; 
            
            // 获取宽/高测量规格的模式 & 测量大小
            int specMode = MeasureSpec.getMode(measureSpec);  
            int specSize = MeasureSpec.getSize(measureSpec);  
          
            switch (specMode) {  
                // 模式为UNSPECIFIED时,使用提供的默认大小 = 参数Size
                case MeasureSpec.UNSPECIFIED:  
                    result = size;  
                    break;  

                // 模式为AT_MOST,EXACTLY时,使用View测量后的宽/高值 = measureSpec中的Size
                case MeasureSpec.AT_MOST:  
                case MeasureSpec.EXACTLY:  
                    result = specSize;  
                    break;  
            }  

         // 返回View的宽/高值
            return result;  
    } 

二. getWidth() / getHeight()返回值

2.1 结论
返回的值是 View在Layout过程中的宽 / 高,即最终的宽 / 高
2.2 源码分析
1、由于getWidth()与getHeight()同理,下面只讲解getWidth()。
2、请务必先了解自定义View的Layout过程:自定义View Layout过程 - 最易懂的自定义View原理系列(3)

public final int getWidth() {  
      return mRight - mLeft;  
      // mRight、mLeft的值赋值是在layout过程中的setFrame()->分析1
  }  

  /**
  * 分析1:setFrame()
  * 作用:根据传入的4个位置值,设置View本身的四个顶点位置
  * 即:最终确定View本身的位置
  */ 
  protected boolean setFrame(int left, int top, int right, int bottom) {
        ...
    // 特别注意:就是这里赋值mRight、mLeft的
    // 通过以下赋值语句记录下了视图的位置信息,即确定View的四个顶点
    // 从而确定了视图的位置
    mLeft = left;
    mTop = top;
    mRight = right;
    mBottom = bottom;

    // setFrame()的参数left、right是从在layout()调用时传入的 -> 分析2

    }

/**
  * 分析2:layout()
  * 作用:确定View本身的位置,即设置View本身的四个顶点位置
  */ 
  public void layout(int l, int t, int r, int b) {  

	... 
      
    // 特别注意
    boolean changed = isLayoutModeOptical(mParent) ?
            setOpticalFrame(l, t, r, b) : setFrame(l, t, r, b);
    // 而l、t、r、b,则是传入的View的四个顶点边界值
    // getWidth()的返回值 = mRight - mLeft = r - l = 子View的右边界 - 子view的左边界 = 宽
  ...

}   

2.3 总结
Android自定义View5--getWidth()和getMeasuredWidth()区别, view.post(Runnable)引发内存泄漏的原因和解决

三. 应用场景

1、getMeasuredWidth() / getMeasuredHeight()是在Measure过程中赋值的,所以需在Measure过程后获取的值才有意义
2、同理,getWidth() / getHeight()在Layout过程中赋值,所以在Layout过程后获取的值才有意义
所以,二者的应用场景是:
1、getMeasuredWidth() / getMeasuredHeight():在onLayout()中获取View的宽/高
2、getWidth() / getHeight():在除onLayout()外的地方获取View的宽/高

四、 额外注意

4.1 不相等情况

问:上面提到,一般情况下,二者获取的宽 / 高是相等的。那么,“非一般” 情况是什么?(即二者不相等)
答:人为设置:通过重写View的 layout()强行设置

@Override
public void layout( int l , int t, int r , int b){
   // 改变传入的顶点位置参数
   super.layout(l,t,r+100,b+100)}

效果:在任何情况下,getWidth() / getHeight()获得的宽/高 总比 getMeasuredWidth() / getMeasuredHeight()获取的宽/高大100px
即:View的最终宽/高 总比 测量宽/高 大100px

虽然这样的人为设置无实际意义,但证明了:View的最终宽 / 高 与 测量宽 / 高是可以不一样。

4.2 辟谣

网上流传这么一个原因描述二者的值的关系:

1、在当屏幕可包裹内容时,他们的值是相等的;
2、 只有当view超出屏幕后,才能看出他们的区别:当超出屏幕后getMeasuredWidth() = getWidth() + 屏幕之外没有显示的大小,即:getMeasuredWidth()是实际View的大小,与屏幕无关;而getHeight的大小此时则是屏幕的大小。

第二条是错误的,正确的是getWidth和getMeasuredWidth的值一直相等,通过实例来证实:

1、内容layout_width设置为wrap_content,内容设置很多超过屏幕:

  <TextView
        android:id="@+id/tv_test"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈哈" />

因为有父布局的match_parent的影响,打印的就是屏幕宽度:

2020-08-20 15:02:10.990 15654-15654/com.liuniukeji.lcsh E/measurewidth: 1080
2020-08-20 15:02:10.990 15654-15654/com.liuniukeji.lcsh E/measurewidth: 1080

2、宽度固定,超过屏幕:

   <TextView
        android:id="@+id/tv_test"
        android:layout_width="3000px"
        android:layout_height="wrap_content"
        android:text="aa" />
2020-08-20 15:36:17.361 12039-12039/com.liuniukeji.lcsh E/measurewidth: 3000
2020-08-20 15:36:17.361 12039-12039/com.liuniukeji.lcsh E/measurewidth: 3000

最终结论:在非人为设置的情况下,getWidth() / getHeight()获得的宽高(View的最终宽/高)与 getMeasuredWidth() / getMeasuredHeight()获得的宽/高(View的测量宽/高 )永远是相等的。

五、总结

下面,用一张图总结getMeasuredWidth() / getMeasuredHeight() 与 getWidth() / getHeight()的区别:

Android自定义View5--getWidth()和getMeasuredWidth()区别, view.post(Runnable)引发内存泄漏的原因和解决

六、 view.post(new Runnable(){})引发内存泄漏的原因和解决:

在onCreate()中获取getWidth()或者getMeasuredWidth()是0,是因为getMeasuredWidth需要在onMeasure()之后能获取到,getWidth()是在onLayout之后才能获取到,很多人用view.post(new Runnable(){})来解决。或者一些操作需要延迟执行,也会用View.post(),但是我在想会不会引发内存泄漏,于是就探索了一下,发现my sun 真的会。先说结论:4.4-5.2的机器会内存泄漏。

顺着源码去看,API28为例:
1、View.post源码:

public boolean post(Runnable action) {
        final AttachInfo attachInfo = mAttachInfo;
        if (attachInfo != null) {
        ///1
            return attachInfo.mHandler.post(action);
        }
        getRunQueue().post(action);
        ///2
        return true;
    }

如果 mAttachInfo 不为空,那就调用 mAttachInfo.mHanlder.post() 方法,如果为空,则调用 getRunQueue().post() 方法。

那就找一下,mAttachInfo 是什么时候赋值的,可以借助 AS 的 Ctrl + F 查找功能,过滤一下 mAttachInfo =,注意 = 号后面还有一个空格,否则你查找的时候会发现全文有两百多处匹配到。我们只关注它是什么时候赋值的,使用的场景就不管了,所以过滤条件可以细一点。这样一来,全文就只有两处匹配:

//View#dispatchAttachedToWindow() 
void dispatchAttachedToWindow(AttachInfo info, int visibility) { 
    mAttachInfo = info;  //唯一赋值的地方 
    //... 
} 

//View#dispatchDetachedFromWindow() 
void dispatchDetachedFromWindow() { 
    //... 
    mAttachInfo = null;  //唯一置空的地方 
    //... 
}

一处赋值,一处置空,刚好又是在对应的一个生命周期里:
1、dispatchAttachedToWindow() 下文简称 attachedToWindow
2、dispatchDetachedFromWindow() 下文简称 detachedFromWindow。

所以,如果 mAttachInfo 不为空的时候,走的就是 Handler 的 post(),也就是 View.post() 在这种场景下,实际上就是调用的 Handler.post(),接下去就是搞清楚一点,这个 Handler 是哪里的 Handler,在哪里初始化等等,但这点可以先暂时放一边,因为 mAttachInfo 是在 attachedToWindow 时才赋值的,所以接下去关键的一点是搞懂 attachedToWindow 到 detachedFromWindow 这个生命周期分别在什么时候在哪里被调用了。

虽然我们现在还不清楚,attachedToWindow 到底是什么时候被调用的,但看到这里我们至少清楚一点,在 Activity 的 onCreate() 期间,这个 View 的 attachedToWindow 应该是还没有被调用,也就是 mAttachInfo 这时候还是为空,但我们在 onCreate() 里执行 View.post() 里的操作仍然可以保证是在 View 宽高计算完毕的,为什么 View.post() 可以保证操作是在 View 宽高计算完毕之后呢?那么这点的原理显然就是在另一个 return 那边的方法里了:getRunQueue().post()。

跟进 getRunQueue() 看看:

//View#getRunQueue()   
private HandlerActionQueue getRunQueue() { 
    if (mRunQueue == null) { 
        mRunQueue = new HandlerActionQueue(); 
    } 
    return mRunQueue; 
}

所以调用的其实是 HandlerActionQueue.post() 方法,那么我们再继续跟进去看看:

public class HandlerActionQueue { 
    private HandlerAction[] mActions; 
    private int mCount; 

    public void post(Runnable action) { 
        //实际上调用postDelayed() 
        postDelayed(action, 0); 
    } 

    public void postDelayed(Runnable action, long delayMillis) { 
        //action作为参数创建一个HandlerAction对象 
        final HandlerAction handlerAction = new HandlerAction(action, delayMillis); 

        synchronized (this) { 
            if (mActions == null) { 
                mActions = new HandlerAction[4]; 
            } 
            //将创建的HandlerAction对象添加进mActions中缓存 
            mActions = GrowingArrayUtils.append(mActions, mCount, handlerAction); 
            mCount++; 
        } 
    } 
}

post(Runnable) 方法内部调用了 postDelayed(Runnable, long),postDelayed() 内部则是将 Runnable 和 long 作为参数创建一个 HandlerAction 对象,然后添加到 mActions 数组里。下面先看看 HandlerAction:

//HandlerActionQueue$HandlerAction 
private static class HandlerAction { 
    //一个实体类,就两个成员变量 
    final Runnable action; 
    final long delay; 
}

很简单的数据结构,就一个 Runnable 成员变量和一个 long 成员变量。这个类作用可以理解为用于包装 View.post(Runnable) 传入的 Runnable 操作的,当然因为还有 View.postDelay() ,所以就还需要一个 long 类型的变量来保存延迟的时间了,这样一来这个数据结构就不难理解了吧。

所以,我们调用 View.post(Runnable) 传进去的 Runnable 操作,在传到 HandlerActionQueue 里会先经过 HandlerAction 包装一下,然后再缓存起来。至于缓存的原理,HandlerActionQueue 是通过一个默认大小为4的数组保存这些 Runnable 操作的,当然,如果数组不够用时,就会通过 GrowingArrayUtils 来扩充数组,具体算法就不继续看下去了,不然越来越偏。

到这里,我们先来梳理下:

当我们在 Activity 的 onCreate() 里执行 View.post(Runnable) 时,因为这时候 View 还没有 attachedToWindow,所以这些 Runnable 操作其实并没有被执行,而是先通过 HandlerActionQueue 缓存起来。

那么到什么时候这些 Runnable 才会被执行呢?我们可以看看 HandlerActionQueue 这个类,它的代码不多,里面有个 executeActions() 方法,看命名就知道,这方法是用来执行这些被缓存起来的 Runnable 操作的:

//HandlerActionQueue#executeActions() 
public void executeActions(Handler handler) { 
    synchronized (this) { 
        final HandlerAction[] actions = mActions; 
        for (int i = 0, count = mCount; i < count; i++) { 
            final HandlerAction handlerAction = actions[i]; 
            //通过handler来执行缓存在mActions中的Runnable操作 
            handler.postDelayed(handlerAction.action, handlerAction.delay); 
        } 

        mActions = null; 
        mCount = 0; 
    } 
}

看到重量级的人物了:Handler。看来被缓存起来没有执行的 Runnable 最后也还是通过 Hnadler 来执行的。那么,这个 Handler 又是哪里的呢?看来关键点还是这个方法在哪里被调用了,借助 AS 的 Ctrl + Alt + F7 快捷键,可以查找 SDK 里的某个方法在哪些地方被调用了。

//View#dispatchAttachedToWindow() 
void dispatchAttachedToWindow(AttachInfo info, int visibility) { 
    mAttachInfo = info;  //唯一赋值的地方 
    //... 
    if(mRunQueue != null) { 
        mRunQueue.executeActions(info.mHandler); 
        mRunQueue = null; 
    } 
    //... 
}

很好,找到了,而且只找到这个地方。其实,这个快捷键有时并没有办法找到一些方法被调用的地方,这也是源码阅读过程中令人头疼的一点,因为没法找到这些方法到底在哪些地方被调用了,所以很难把流程梳理下来。如果方法是私有的,那很好办,就用 Ctrl + F 在这个类里找一下就可以,如果匹配结果太多,那就像开头那样把过滤条件详细一点。如果方法不是私有的,那真的就很难办了,这也是一开始找到 dispatchAttachedToWindow() 后为什么不继续跟踪下去转而来分析Q2:getRunQueue() 的原因,因为用 AS 找不到 dispatchAttachedToWindow() 到底在哪些地方被谁调用了。哇,好像又扯远了,回归正题回归正题。

emmm,看来这里也绕回来了,dispatchAttachedToWindow() 看来是个关键的节点。

那到这里,我们再次来梳理一下:

我们使用 View.post() 时,其实内部它自己分了两种情况处理,当 View 还没有 attachedToWindow 时,通过 View.post(Runnable) 传进来的 Runnable 操作都先被缓存在 HandlerActionQueue,然后等 View 的 dispatchAttachedToWindow() 被调用时,就通过 mAttachInfo.mHandler 来执行这些被缓存起来的 Runnable 操作。从这以后到 View 被 detachedFromWindow 这段期间,如果再次调用 View.post(Runnable) 的话,那么这些 Runnable 不用再缓存了,而是直接交给 mAttachInfo.mHanlder 来执行。

以上,就是到目前我们所能得知的信息。这样一来,Q2 是不是渐渐有一些头绪了:View.post(Runnable) 的操作之所以可以保证肯定是在 View 宽高计算完毕之后才执行的,是因为这些 Runnable 操作只有在 View 的 attachedToWindow 到 detachedFromWiondow 这期间才会被执行。

那么,接下去就还剩两个关键点需要搞清楚了:

1、dispatchAttachedToWindow() 是什么时候被调用的?
2、mAttachInfo 是在哪里初始化的?

只借助 AS 的话,很难找到 dispatchAttachedToWindow() 到底在哪些地方被调用。所以,到这里,我又借助了 Source Insight 软件。

Android自定义View5--getWidth()和getMeasuredWidth()区别, view.post(Runnable)引发内存泄漏的原因和解决很棒!找到了四个被调用的地方,三个在 ViewGroup 里,一个在 ViewRootImpl.performTraversals() 里。找到了就好,接下去继续用 AS 来分析吧,Source Insight 用不习惯,不过分析源码时确实可以结合这两个软件。

//ViewRootImpl#performTraversals() 
private void performTraversals() { 
    final View host = mView;  //mView 是 Activity 的 DecorView 
    //... 

    if(mFirst) { 
        //... 
        host.dispatchAttachedToWindow(mAttachInfo, 0); 
        //... 
    } 
    //... 
}

就想看个 View.post(),结果跟着跟着,跟到这里来了。ViewRootImpl 我在分析Android KeyEvent 点击事件分发处理流程时短暂接触过,地址如下所示:

https://www.jianshu.com/p/2f28386706a0

mView 是 Activity 的 DecorView。咦~,等等,这样看来 ViewRootImpl 是调用的 DecorView 的 dispatchAttachedToWindow() ,但我们在使用 View.post() 时,这个 View 可以是任意 View,并不是非得用 DecorView 吧。哈哈哈,这是不是代表着我们找错地方了?不管了,我们就去其他三个被调用的地方: ViewGroup 里看看吧:

//ViewGroup#addViewInner() 
private void addViewInner(View child, int index, LayoutParams params, boolean preventRequestLayout) { 
    //...   

    AttachInfo ai = mAttachInfo; 
    if(ai != null && (mGroupFlags & FLAG_PREVENT_DISPATCH_ATTACHED_TO_WINDOW) == 0) { 
        //... 
        child.dispatchAttachedToWindow(mAttachInfo, (mViewFlags&VISIBILITY_MASK)); 
        //... 
    } 
    //... 
}

addViewInner() 是 ViewGroup 在添加子 View 时的内部逻辑,也就是说当 ViewGroup addView() 时,如果 mAttachInfo 不为空,就都会去调用子 View 的 dispatchAttachedToWindow(),并将自己的 mAttachInfo 传进去。还记得 View 的 dispatchAttachedToWindow() 这个方法么:

//View#dispatchAttachedToWindow() 
void dispatchAttachedToWindow(AttachInfo info, int visibility) { 
    mAttachInfo = info;  //唯一赋值的地方 
    //... 
}

mAttachInfo 唯一被赋值的地方也就是在这里,那么也就是说,子 View 的 mAttachInfo 其实跟父控件 ViewGroup 里的 mAttachInfo 是同一个的。那么,关键点还是这个 mAttachInfo 什么时候才不为空,也就是说 ViewGroup 在 addViewInner() 时,传进去的 mAttachInfo 是在哪被赋值的呢?我们来找找看:

Android自定义View5--getWidth()和getMeasuredWidth()区别, view.post(Runnable)引发内存泄漏的原因和解决

咦,利用 AS 的 Ctrl + 左键 怎么找不到 mAttachInfo 被定义的地方呢,不管了,那我们用 Ctrl + F 搜索一下在 ViewGroup 类里 mAttachInfo 被赋值的地方好了:
Android自定义View5--getWidth()和getMeasuredWidth()区别, view.post(Runnable)引发内存泄漏的原因和解决咦,怎么一个地方也没有。难道说,这个 mAttachInfo 是父类 View 定义的变量么,既然 AS 找不到,我们换 Source Insight 试试:

Android自定义View5--getWidth()和getMeasuredWidth()区别, view.post(Runnable)引发内存泄漏的原因和解决
Android自定义View5--getWidth()和getMeasuredWidth()区别, view.post(Runnable)引发内存泄漏的原因和解决还真的是,ViewGroup 是继承的 View,并且处于同一个包里,所以可以直接使用该变量,那这样一来,我们岂不是又绕回来了。前面说过,dispatchAttachedToWindow() 在 ViewGroup 里有三处调用的地方,既然 addViewInner() 这里的看不出什么,那去另外两个地方看看:

//ViewGroup#dispatchAttachedToWindow() 
@Override 
void dispatchAttachedToWindow(AttachInfo info, int visibility) { 
    //... 
    super.dispatchAttachedToWindow(info, visibility); 
    //... 

    final int count = mChildrenCount; 
    final View[] children = mChildren; 
    for (int i = 0; i < count; i++) { 
        final View child = children[i]; 
        //通知所有子View 
        child.dispatchAttachedToWindow(info, combineVisibility(visibility, child.getVisibility())); 
    } 
    //...
剩下的两个地方就都是在 ViewGroup 重写的 dispatchAttachedToWindow() 方法里了,这代码也很好理解,在该方法被调用的时候,先执行 super 也就是 View 的 dispatchAttachedToWindow() 方法,还没忘记吧,mAttachInfo 就是在这里被赋值的。然后再遍历子 View,分别调用子 View 的 dispatchAttachedToWindow() 方法,并将 mAttachInfo 作为参数传递进去,这样一来,子 View 的 mAttachInfo 也都被赋值了。

但这样一来,我们就绕进死胡同了。我们还是先来梳理一下吧:

目前,我们知道,View.post(Runnable) 的这些 Runnable 操作,在 View 被 attachedToWindow 之前会先缓存下来,然后在 dispatchAttachedToWindow() 被调用时,就将这些缓存下来的 Runnable 通过 mAttachInfo 的 mHandler 来执行。在这之后再调用 View.post(Runnable) 的话,这些 Runnable 操作就不用再被缓存了,而是直接交由 mAttachInfo 的 mHandler 来执行。

所以,我们得搞清楚 dispatchAttachedToWindow() 在什么时候被调用,以及 mAttachInfo 是在哪被初始化的,因为需要知道它的变量如 mHandler 都是些什么以及验证 mHandler 执行这些 Runnable 操作是在 measure 之后的,这样才能保证此时的宽高不为0。

然后,我们在跟踪 dispatchAttachedToWindow() 被调用的地方时,跟到了 ViewGroup 的 addViewInner() 里。在这里我们得到的信息是如果 mAttachInfo 不为空时,会直接调用子 View 的 dispatchAttachedToWindow(),这样新 add 进来的子 View 的 mAttachInfo 就会被赋值了。但 ViewGroup 的 mAttachInfo 是父类 View 的变量,所以为不为空的关键还是回到了 dispatchAttachedToWindow() 被调用的时机。

我们还跟到了 ViewGroup 重写的 dispatchAttachedToWindow() 方法里,但显然,ViewGroup 重写这个方法只是为了将 attachedToWindow 这个事件通知给它所有的子 View。

所以,最后我们能得到的结论就是,我们还得再回去 ViewRootImpl 里,dispatchAttachedToWindow() 被调用的地方,除了 ViewRootImpl,我们都分析过了,得不到什么信息,只剩最后 ViewRootImpl 这里了,所以关键点肯定在这里。看来这次,不行也得上了。

//ViewRootImpl#performTraversals() 
private void performTraversals() { 
    final View host = mView;  //mView 是 Activity 的 DecorView 
    //... 

    if(mFirst) { 
        //... 
        host.dispatchAttachedToWindow(mAttachInfo, 0); 
        //... 
    } 
    //... 
}

这方法代码有八百多行!!不过,我们只关注我们需要的点就行,这样一省略无关代码来看,是不是感觉代码就简单得多了。

mFirst 初始化为 true,全文只有一处赋值,所以 if(mFirst) 块里的代码只会执行一次。我对 ViewRootImpl 不是很懂,performTraversals() 这个方法应该是通知 Activity 的 View 树开始测量、布局、绘制。而 DevorView 是 Activity 视图的根布局、View 树的起点,它继承 FrameLayout,所以也是个 ViewGroup,而我们之前对 ViewGroup 的 dispatchAttachedToWindow() 分析过了吧,在这个方法里会将 mAttachInfo 传给所有子 View。也就是说,在 Activity 首次进行 View 树的遍历绘制时,ViewRootImpl 会将自己的 mAttachInfo 通过根布局 DecorView 传递给所有的子 View 。

那么,我们就来看看 ViewRootImpl 的 mAttachInfo 什么时候初始化的吧:

//ViewRootImpl#ViewRootImpl() 
public ViewRootImpl(Context context, Display display) { 
    //... 
    mFirst = true; 
    mAttachInfo = new View.AttachInfo(mWindowSession, mWindow, display, this, mHandler, this); 
    //... 
}

在构造函数里对 mAttachInfo 进行初始化,传入了很多参数,我们关注的应该是 mHandler 这个变量,所以看看这个变量定义:

public final class ViewRootImpl implements ViewParent, View.AttachInfo.Callbacks, ThreadedRenderer.HardwareDrawCallbacks { 
    //成员变量,声明时就初始化 
    final ViewRootHandler mHandler = new ViewRootHandler(); 
}

终于找到 new Handler() 的地方了,至于这个自定义的 Handler 类做了啥,我们不关心,反正通过 post() 方式执行的操作跟它自定义的东西也没有多大关系。我们关心的是在哪 new 了这个 Handler。因为每个 Handler 在 new 的时候都会绑定一个 Looper,这里 new 的时候是无参构造函数,那默认绑定的就是当前线程的 Looper,而这句 new 代码是在主线程中执行的,所以这个 Handler 绑定的也就是主线程的 Looper。至于这些的原理,就涉及到 Handler 的源码和 ThreadLocal 的原理了,就不继续跟进了,太偏了,大伙清楚结论这点就好。

这也就是为什么 View.post(Runnable) 的操作可以更新 UI 的原因,因为这些 Runnable 操作都通过 ViewRootImpl 的 mHandler 切到主线程来执行了。

跟到 ViewRootImpl 这里应该就可以停住了。至于 ViewRootImpl 跟 Activity 有什么关系、什么时候被实例化的、跟 DecroView 如何绑定的就不跟进了,因为我也还不是很懂,感兴趣的可以自己去看看,我在末尾会给一些参考博客。

至此,我们清楚了 mAttachInfo 的由来,也知道了 mAttachInfo.mHandler,还知道在 Activity 首次遍历 View 树进行测量、绘制时会通过 DecorView 的 dispatchAttachedToWindow() 将 ViewRootImpl 的 mAttachInfo 传递给所有子 View,并通知所有调用 View.post(Runnable) 被缓存起来的 Runnable 操作可以执行了。

但不知道大伙会不会跟我一样还有一点疑问:看网上对 ViewRootImpl.performTraversals() 的分析:遍历 View 树进行测量、布局、绘制操作的代码显然是在调用了 dispatchAttachedToWindow() 之后才执行,那这样一来是如何保证 View.post(Runnable) 的 Runnable 操作可以获取到 View 的宽高呢?明明测量的代码 performMeasure() 是在 dispatchAttachedToWindow() 后面才执行。

//ViewRootImpl#performTraversals() 
private void performTraversals() { 
    final View host = mView;  //mView 是 Activity 的 DecorView 
    //... 

    if(mFirst) { 
        //... 
        host.dispatchAttachedToWindow(mAttachInfo, 0); 
        //... 
    } 
    //... 
    //发起测量操作 
    performMeasure(); 

    //... 
    //发起布局操作 
    performLayout(); 

    //... 
    //发起绘制操作 
    performDraw(); 

    //... 
}

我在这里卡了很久,一直没想明白。我甚至以为是 PhoneWindow 在加载 layout 布局到 DecorView 时就进行了测量的操作,所以一直跟,跟到 LayoutInflater.inflate(),跟到了 ViewGroup.addView(),最后发现跟测量有关的操作最终都又绕回到 ViewRootImpl 中去了。

最后通过下面这篇文章解决了我的问题,地址如下:
http://blog.csdn.net/scnuxisan225/article/details/49815269
大概的来讲,就是我们的 app 都是基于消息驱动机制来运行的,主线程的 Looper 会无限的循环,不断的从 MessageQueue 里取出 Message 来执行,当一个 Message 执行完后才会去取下一个 Message 来执行。而 Handler 则是用于将 Message 发送到 MessageQueue 里,等轮到 Message 执行时,又通过 Handler 发送到 Target 去执行,等执行完再取下一个 Message,如此循环下去。

清楚了这点后,我们再回过头来看看:

performTraversals() 会先执行 dispatchAttachedToWindow(),这时候所有子 View 通过 View.post(Runnable) 缓存起来的 Runnable 操作就都会通过 mAttachInfo.mHandler 的 post() 方法将这些 Runnable 封装到 Message 里发送到 MessageQueue 里。mHandler 我们上面也分析过了,绑定的是主线程的 Looper,所以这些 Runnable 其实都是发送到主线程的 MessageQueue 里排队,等待执行。然后 performTraversals() 继续往下工作,相继执行 performMeasure(),performLayout() 等操作。等全部执行完后,表示这个 Message 已经处理完毕,所以 Looper 才会去取下一个 Message,这时候,才有可能轮到这些 Runnable 执行。所以,这些 Runnable 操作也就肯定会在 performMeasure() 操作之后才执行,宽高也就可以获取到了。画张图,帮助理解一下:

Android自定义View5--getWidth()和getMeasuredWidth()区别, view.post(Runnable)引发内存泄漏的原因和解决总结:
1、View.post(Runnable) 内部会自动分两种情况处理,当 View 还没 attachedToWindow 时,会先将这些 Runnable 操作缓存下来;否则就直接通过 mAttachInfo.mHandler 将这些 Runnable 操作 post 到主线程的 MessageQueue 中等待执行。

2、如果 View.post(Runnable) 的 Runnable 操作被缓存下来了,那么这些操作将会在 dispatchAttachedToWindow() 被回调时,通过 mAttachInfo.mHandler.post() 发送到主线程的 MessageQueue 中等待执行。

3、mAttachInfo 是 ViewRootImpl 的成员变量,在构造函数中初始化,Activity View 树里所有的子 View 中的 mAttachInfo 都是 ViewRootImpl.mAttachInfo 的引用。

4、mAttachInfo.mHandler 也是 ViewRootImpl 中的成员变量,在声明时就初始化了,所以这个 mHandler 绑定的是主线程的 Looper,所以 View.post() 的操作都会发送到主线程中执行,那么也就支持 UI 操作了。

5、dispatchAttachedToWindow() 被调用的时机是在 ViewRootImol 的 performTraversals() 中,该方法会进行 View 树的测量、布局、绘制三大流程的操作。

6、Handler 消息机制通常情况下是一个 Message 执行完后才去取下一个 Message 来执行(异步 Message 还没接触),所以 View.post(Runnable) 中的 Runnable 操作肯定会在 performMeaure() 之后才执行,所以此时可以获取到 View 的宽高。

使用 View.post(),还是有可能会造成内存泄漏的,Handler 会造成内存泄漏的原因是由于内部类持有外部的引用,如果任务是延迟的,就会造成外部类无法被回收。而根据我们的分析,mAttachInfo.mHandler 只是 ViewRootImpl 一个内部类的实例,所以使用不当还是有可能会造成内存泄漏的。

七、getWidth()怎么获取?

完全可以在onWindowFocusChanged方法中获取:

android onWindowsFocusChanged()

@Override
    public void onWindowFocusChanged(boolean hasFocus) {
        super.onWindowFocusChanged(hasFocus);
    }

本文地址:https://blog.csdn.net/u013750244/article/details/108086825