深入解析Android中View创建的全过程

前言

吸进这几天在看View的尺寸是怎样计算出来的,于是看了整个View被初始化的过程,结合系统源码总结了一下分享出来,方便需要的朋友或者自己以后有需要的时候看看,下面话不多说了,来看看详细的介绍吧。

从布局文件到LayoutParams

首先从Activity的setContentView(int)方法开始,只要设置了R.layout的布局文件,那么界面上就会显示出来对应的内容。所以以这个方法为初发点,然后往后跟踪代码。

public void setContentView(@LayoutRes int layoutResID) {
 getWindow().setContentView(layoutResID);
 initWindowDecorActionBar();
}

通过以上代码发现调用了Window类的setContentView方法,那么这个Window对象mWindow又是怎么初始化的?在Activity中搜索发现是在Activity的attach方法中初始化的,构造了一个PhoneWindow对象。

如下代码所示:

final void attach(Context context, ActivityThread aThread,
  Instrumentation instr, IBinder token, int ident,
  Application application, Intent intent, ActivityInfo info,
  CharSequence title, Activity parent, String id,
  NonConfigurationInstances lastNonConfigurationInstances,
  Configuration config, String referrer, IVoiceInteractor voiceInteractor) {
 attachBaseContext(context);
 mFragments.attachHost(null /*parent*/);
 mWindow = new PhoneWindow(this); // 这里创建了Window对象
 mWindow.setCallback(this);
 mWindow.setOnWindowDismissedCallback(this);
 mWindow.getLayoutInflater().setPrivateFactory(this);
 // ... 中间部分代码省略
 mWindow.setWindowManager(
   (WindowManager)context.getSystemService(Context.WINDOW_SERVICE),
   mToken, mComponent.flattenToString(),
   (info.flags & ActivityInfo.FLAG_HARDWARE_ACCELERATED) != 0);
 if (mParent != null) {
  mWindow.setContainer(mParent.getWindow());
 }
 mWindowManager = mWindow.getWindowManager();
 mCurrentConfig = config;
}

在PhoneWindow的setContentView(int)方法中,发现是调用了LayoutInflater的inflate(int, View)方法,对这个布局文件进行转换成View,并添加到后面View这个参数中。

@Override
public void setContentView(int layoutResID) {
 // Note: FEATURE_CONTENT_TRANSITIONS may be set in the process of installing the window
 // decor, when theme attributes and the like are crystalized. Do not check the feature
 // before this happens.
 if (mContentParent == null) {
  installDecor();
 } else if (!hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
  mContentParent.removeAllViews();
 }
 if (hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
  // ...
 } else {
  mLayoutInflater.inflate(layoutResID, mContentParent);
 }
 // ...
}

这里面顺带穿插说一下View的根节点是怎样初始化出来的。

这里有一个关键的地方是这个installDecor()方法,在这个方法中通过调用generateDecor()方法创建了这个mDecor的对象,通过调用generateLayout(DecorView)方法初始化出来了mContentParent对象。其中,mDecor是PhoneWindow.DecorView的类实例,mContentParent是展示所有内容的,是通过com.android.internal.R.id.contentID来找到这个View。

具体代码如下所示:

protected ViewGroup generateLayout(DecorView decor) {
 // ...
 View in = mLayoutInflater.inflate(layoutResource, null); // layoutResource是根据对当前显示View的Activity的theme属性值来决定由系统加载对应的布局文件
 decor.addView(in, new ViewGroup.LayoutParams(MATCH_PARENT, MATCH_PARENT));
 mContentRoot = (ViewGroup) in;

 ViewGroup contentParent = (ViewGroup)findViewById(ID_ANDROID_CONTENT);
 if (contentParent == null) {
 throw new RuntimeException("Window couldn't find content container view");
 }
 // ...
 return contentParent;
}

那么在哪里面可以看到这个DecorView呢?看下面图。

下面这张图是在debug模式下连接手机调试App,使用Layout Inspector工具查看得到的图:


PhoneWindow.DecorView

其中从1位置可以看出,整个View的根节点的View是PhoneWindow.DecorView实例;从2位置和3位置的mId可以推断出来,上面的mContentParent就是ContentFrameLayout类的实例;位置4中的蓝色区域是mContentParent所表示的位置和大小。

以上图是在AS 2.2.3版本上使用Android Monitor Tab页中的Layout Inspector工具(参考位置5)生成。

紧接着跟踪上面LayoutInflater中的inflate()方法中调用,发现最后调用到了
inflate(@LayoutRes int resource, @Nullable ViewGroup root, boolean attachToRoot)方法中,在这个方法中构造了XmlResourceParser对象,而这个parser对象构造代码如下所示:

XmlResourceParser loadXmlResourceParser(int id, String type)
  throws NotFoundException {
 synchronized (mAccessLock) {
  TypedValue value = mTmpValue;
  if (value == null) {
   mTmpValue = value = new TypedValue();
  }
  getValue(id, value, true);
  if (value.type == TypedValue.TYPE_STRING) {
   return loadXmlResourceParser(value.string.toString(), id,
     value.assetCookie, type);
  }
  throw new NotFoundException(
    "Resource ID #0x" + Integer.toHexString(id) + " type #0x"
    + Integer.toHexString(value.type) + " is not valid");
 }
}
XmlResourceParser loadXmlResourceParser(String file, int id,
  int assetCookie, String type) throws NotFoundException {
 if (id != 0) {
  // ...
  // These may be compiled...
  synchronized (mCachedXmlBlockIds) {
   // First see if this block is in our cache.
   final int num = mCachedXmlBlockIds.length;
   for (int i=0; i<num; i++) {
    if (mCachedXmlBlockIds[i] == id) {
     //System.out.println("**** REUSING XML BLOCK! id="
     //     + id + ", index=" + i);
     return mCachedXmlBlocks[i].newParser();
    }
   }
   // Not in the cache, create a new block and put it at
   // the next slot in the cache.
   XmlBlock block = mAssets.openXmlBlockAsset(
     assetCookie, file);
   if (block != null) {
    int pos = mLastCachedXmlBlockIndex+1;
    if (pos >= num) pos = 0;
    mLastCachedXmlBlockIndex = pos;
    XmlBlock oldBlock = mCachedXmlBlocks[pos];
    if (oldBlock != null) {
     oldBlock.close();
    }
    mCachedXmlBlockIds[pos] = id;
    mCachedXmlBlocks[pos] = block;
    //System.out.println("**** CACHING NEW XML BLOCK! id="
    //     + id + ", index=" + pos);
    return block.newParser();
   }
  }
  // ...
 }
 // ...
}

其中getValue()方法调用到本地frameworks/base/core/jni/android_util_AssetManager.cpp文件中的static jint android_content_AssetManager_loadResourceValue函数,而在这个函数中java层的TypeValue的类对象value对象属性通过JNI形式被赋值。

在构建这个parser时,经历了很复杂的过程,从TypeValue中的assetCookie属性,到XmlBlock中的xmlBlock属性,再到XmlBlock.Parser中的mParseState属性,都是用来保存JNI层的数据,因为最终操作这些资源数据是在native层,所以必不可少通过JNI这种方式,在java层和native层周旋。这里没有深入到native层去分析资源是如何被加载解析的,后面有机会再说。

最后跟到了这个public View inflate(XmlPullParser parser, @Nullable ViewGroup root, boolean attachToRoot)方法中,代码如下所示:

public View inflate(XmlPullParser parser, @Nullable ViewGroup root, boolean attachToRoot) {
 synchronized (mConstructorArgs) {
 final Context inflaterContext = mContext;
 final AttributeSet attrs = Xml.asAttributeSet(parser);
 Context lastContext = (Context) mConstructorArgs[0];
 View result = root;
 // ...

 // Temp is the root view that was found in the xml
 final View temp = createViewFromTag(root, name, inflaterContext, attrs); // 这里将布局文件中的名称反射成具体的View类对象
 ViewGroup.LayoutParams params = null;
 if (root != null) {
  // Create layout params that match root, if supplied
  params = root.generateLayoutParams(attrs); // 这里将尺寸转换成了LayoutParams
  if (!attachToRoot) {
   // Set the layout params for temp if we are not
   // attaching. (If we are, we use addView, below)
   temp.setLayoutParams(params);
  }
 }
 // Inflate all children under temp against its context.
 rInflateChildren(parser, temp, attrs, true);
 // We are supposed to attach all the views we found (int temp)
 // to root. Do that now.
 if (root != null && attachToRoot) {
  root.addView(temp, params); // 将布局文件中根的View添加到mContentParent中
 }

 // ...
 return result;
}

接着看View的generateLayoutParams(AttributeSet)方法,因为这里返回了params。查看代码最后发现LayoutParams的width和height属性赋值的代码如下所示:

public LayoutParams(Context c, AttributeSet attrs) {
 TypedArray a = c.obtainStyledAttributes(attrs, R.styleable.ViewGroup_Layout);
 setBaseAttributes(a,
   R.styleable.ViewGroup_Layout_layout_width,
   R.styleable.ViewGroup_Layout_layout_height);
 a.recycle();
}

protected void setBaseAttributes(TypedArray a, int widthAttr, int heightAttr) {
 width = a.getLayoutDimension(widthAttr, "layout_width");
 height = a.getLayoutDimension(heightAttr, "layout_height");
}

通过查看TypedArray类中的getLayoutDimension()方法发现,获取的值是通过index在mData这个成员数组中获取的。这个mData的值是在创建TypedArray对象时被赋的值,具体参见TypedArray的obtain方法。这个数组是在Resources的obtainStyledAttributes()方法中通过调用AssetManager.applyStyle()方法被初始化值的。applyStyle()方法是一个native方法,对应frameworks/base/core/jni/android_util_AssetManager.cpp文件中的android_content_AssetManager_applyStyle函数。在这个函数中发现,传入的Resrouces类中的mTheme成员以及XmlBlock.Parse类的mParseState成员都是一个C++对象的指针,在java类中以整型或长整型值保存。

至此,布局文件中的尺寸值已经被转换成了具体的int类型值。

从布局文件到View

从上面的public View inflate(XmlPullParser parser, @Nullable ViewGroup root, boolean attachToRoot)方法中看到View是通过这行代码final View temp = createViewFromTag(root, name, inflaterContext, attrs);创建出来的,而这个name就是XML文件在解析时遇到的标签名称,比如TextView。此时的attrs也就是上面分析的XmlBlock.Parser的对象。最后发现View是在createViewFromTag()方法中创建的,代码如下所示:

View createViewFromTag(View parent, String name, Context context, AttributeSet attrs,
  boolean ignoreThemeAttr) {
 if (name.equals("view")) {
  name = attrs.getAttributeValue(null, "class");
 }

 // Apply a theme wrapper, if allowed and one is specified.
 if (!ignoreThemeAttr) {
  final TypedArray ta = context.obtainStyledAttributes(attrs, ATTRS_THEME);
  final int themeResId = ta.getResourceId(0, 0);
  if (themeResId != 0) {
   context = new ContextThemeWrapper(context, themeResId);
  }
  ta.recycle();
 }
 // ...
 View view;
 if (mFactory2 != null) {
  view = mFactory2.onCreateView(parent, name, context, attrs);
 } else if (mFactory != null) {
  view = mFactory.onCreateView(name, context, attrs);
 } else {
  view = null;
 }
 if (view == null && mPrivateFactory != null) {
  view = mPrivateFactory.onCreateView(parent, name, context, attrs);
 }
 if (view == null) {
  final Object lastContext = mConstructorArgs[0];
  mConstructorArgs[0] = context;
  try {
   if (-1 == name.indexOf('.')) {
    view = onCreateView(parent, name, attrs);
   } else {
    view = createView(name, null, attrs);
   }
  } finally {
   mConstructorArgs[0] = lastContext;
  }
 }
 return view;

 // ...
}

这里要注意一下,mConstructorArgs的第一个值是一个Context,而这个Context有可能已经不是当前Activity的Context。

看到这里,下面这样的代码片段是不是很熟悉?

if (name.equals("view")) {
 name = attrs.getAttributeValue(null, "class");
}

上面Factory这个接口对象在LayoutInflater类中就有三个属性,分别对应:factory、factory2、mPrivateFactory。很明显,弄清了这三个对象,也就知道了View的初始化流程。下面代码是对这三个属性的值的输出:

public class MainActivity extends AppCompatActivity {
 @Override
 protected void onCreate(Bundle savedInstanceState) {
  super.onCreate(savedInstanceState);
  setContentView(R.layout.activity_main);
  LayoutInflater inflater = getLayoutInflater();
  LayoutInflater inflater1 = LayoutInflater.from(this);
  Field f = null;
  try {
   f = LayoutInflater.class.getDeclaredField("mPrivateFactory");
   f.setAccessible(true);
  } catch (NoSuchFieldException e) {
   e.printStackTrace();
  }
 Log.d("may", "the same object: " + (inflater == inflater1));
  Log.d("may", "inflater factory: " + inflater.getFactory() + ", factory2: " + inflater.getFactory2());
  Log.d("may", "inflater1 factory: " + inflater1.getFactory() + ", factory2: " + inflater1.getFactory2());
  if (f != null) {
   try {
    Log.d("may", "inflater mPrivateFactory: " + f.get(inflater));
    Log.d("may", "inflater1 mPrivateFactory: " + f.get(inflater1));
   } catch (IllegalAccessException e) {
    e.printStackTrace();
   }
  }
 }
}

输出的LOG如下所示:

// 当前Activiy继承的是android.support.v7.app.AppCompatActivity
the same object: true
inflater factory: android.support.v4.view.LayoutInflaterCompatHC$FactoryWrapperHC{android.support.v7.app.AppCompatDelegateImplV14@41fdf0b0}, factory2: android.support.v4.view.LayoutInflaterCompatHC$FactoryWrapperHC{android.support.v7.app.AppCompatDelegateImplV14@41fdf0b0}
inflater1 factory: android.support.v4.view.LayoutInflaterCompatHC$FactoryWrapperHC{android.support.v7.app.AppCompatDelegateImplV14@41fdf0b0}, factory2: android.support.v4.view.LayoutInflaterCompatHC$FactoryWrapperHC{android.support.v7.app.AppCompatDelegateImplV14@41fdf0b0}
inflater mPrivateFactory: com.jacpy.sb.MainActivity@41fd9e70
inflater1 mPrivateFactory: com.jacpy.sb.MainActivity@41fd9e70
// 当前Activity继承的是android.app.Activity
the same object: true
inflater factory: null, factory2: null
inflater1 factory: null, factory2: null
inflater mPrivateFactory: com.jacpy.sb.MainActivity@41fd9a28
inflater1 mPrivateFactory: com.jacpy.sb.MainActivity@41fd9a28

首先看到mPrivateFactory是当前的Activity实例,因为Activity也实现的Factory2接口。首先看LayoutInflater的创建过程,如下图所示:

LayoutInflater初始化流程

而生成的PhoneLayoutInflater对象是缓存在ContextImpl类的属性SYSTEM_SERVICE_MAP中,所以通过Context.LAYOUT_INFLATER_SERVIC去取,始终是同一个对象,当然仅限于当前Context中。

mPrivateFactory属性的赋值是在Activity的attach()方法中,通过调用mWindow.getLayoutInflater().setPrivateFactory(this); ,因此调用Factory2的onCreateView()方法时,实际是调用Activity中的onCreateView()方法。而Activity中的onCreateView()实际返回的是null,所以最后创建View的是if判断中的onCreateView(parent, name, attrs)方法,最后View是在LayoutInflater类中的public final View createView(String name, String prefix, AttributeSet attrs) throws ClassNotFoundException, InflateException方法中创建:

public final View createView(String name, String prefix, AttributeSet attrs)
  throws ClassNotFoundException, InflateException {
 Constructor<? extends View> constructor = sConstructorMap.get(name);
 Class<? extends View> clazz = null;
 // ...
 // Class not found in the cache, see if it's real, and try to add it
 clazz = mContext.getClassLoader().loadClass(
   prefix != null ? (prefix + name) : name).asSubclass(View.class); // prefix传的值是android.view.

 // ...

 constructor = clazz.getConstructor(mConstructorSignature); // Class<?>[] mConstructorSignature = new Class[] {
   Context.class, AttributeSet.class};
 constructor.setAccessible(true);
 sConstructorMap.put(name, constructor);
 // ...
 Object[] args = mConstructorArgs;
 args[1] = attrs; // 这个值是XmlBlock.Parser对象
 final View view = constructor.newInstance(args);
 if (view instanceof ViewStub) {
  // Use the same context when inflating ViewStub later.
  final ViewStub viewStub = (ViewStub) view;
  viewStub.setLayoutInflater(cloneInContext((Context) args[0]));
 }
 return view;
}

这里有没有发现mConstructorSignature数组的长度决定了调用了View的哪个构造方法?

总结

好了,以上就是这篇文章的全部内容了,至此,View已经创建成功。希望本文的内容对各位Android开发者们能带来一定的帮助,如果有疑问大家可以留言交流。

(0)

相关推荐

  • Android View移动的六种方法小结

    在android开发中,经常会遇到一个view需要它能够支持滑动的需求.下面通过本篇文章给大家介绍android view移动的六种方法. layout() 如果你将滑动后的目标位置的坐标传递给layout(),这样子就会把view的位置给重新布置了一下,在视觉上就是view的一个滑动的效果. public class DragView extends View{ private int lastX; private int lastY; public DragView(Context cont

  • Android 自定义View的使用介绍

    在项目开发中,可能系统自带的一些widget不能满足我们的需求,这时就需要自定义View. 通过查看系统中的常用widget如Button,TextView,EditText,他们都继承自View,所以我们在继承自定义View的时候也自然的需要继承View.1.首先新建一个类LView继承自View 复制代码 代码如下: public class LView extends View { private Paint paint; public LView(Context context) {  

  • android开发教程之view组件添加边框示例

    给TextureView添加边框(专业名词为描边),有三种解决方案: 1.设置一个9 patch 的,右边框,中间是空的PNG. 2.自定义一个View,用Canvas画个边框. 3.用Android提供的ShapeDrawable来定义一个边框. 个人比较建议采用第三种方式,原因是因为第三种只要写XML,速度快,占用资源小,代码编写量也少,便于维护. 使用方法如下: 1.定义一个background.xml文件. 复制代码 代码如下: <?xml version="1.0" e

  • Android动态添加View的问题解决方法

    后台代码 复制代码 代码如下: private void ChangeView()    {        ly.removeAllViews();        LayoutInflater inflater = (LayoutInflater)getSystemService(LAYOUT_INFLATER_SERVICE);        View layout = inflater.inflate(R.layout.grid,null);        GridView gridview

  • 深入解析Android中View创建的全过程

    前言 吸进这几天在看View的尺寸是怎样计算出来的,于是看了整个View被初始化的过程,结合系统源码总结了一下分享出来,方便需要的朋友或者自己以后有需要的时候看看,下面话不多说了,来看看详细的介绍吧. 从布局文件到LayoutParams 首先从Activity的setContentView(int)方法开始,只要设置了R.layout的布局文件,那么界面上就会显示出来对应的内容.所以以这个方法为初发点,然后往后跟踪代码. public void setContentView(@LayoutRe

  • 从源码解析Android中View的容器ViewGroup

    这回我们是深入到ViewGroup内部\,了解ViewGroup的工作,同时会阐述更多有关于View的相关知识.以便为以后能灵活的使用自定义空间打更近一步的基础.希望有志同道合的朋友一起来探讨,深入Android内部,深入理解Android. 一.ViewGroup是什么?        一个ViewGroup是一个可以包含子View的容器,是布局文件和View容器的基类.在这个类里定义了ViewGroup.LayoutParams类,这个类是布局参数的子类. 其实ViewGroup也就是Vie

  • 解析Android中View转换为Bitmap及getDrawingCache=null的解决方法

    1.前言 Android中经常会遇到把View转换为Bitmap的情形,比如,对整个屏幕视图进行截屏并生成图片:Coverflow中需要把一页一页的view转换为Bitmap.以便实现复杂的图形效果(阴影.倒影效果等):再比如一些动态的实时View为便于观察和记录数据.需要临时生成静态的Bitmap. 2.实现方法 1)下面是笔者经常用的一个转换方法 public static Bitmap convertViewToBitmap(View view, int bitmapWidth, int

  • Android中View绘制流程详细介绍

    创建Window Window即窗口,这个概念在AndroidFramework中的实现为android.view.Window这个抽象类,这个抽象类是对Android系统中的窗口的抽象.在介绍这个类之前,我们先来看看究竟什么是窗口呢? 实际上,窗口是一个宏观的思想,它是屏幕上用于绘制各种UI元素及响应用户输入事件的一个矩形区域.通常具备以下两个特点: 独立绘制,不与其它界面相互影响: 不会触发其它界面的输入事件: 在Android系统中,窗口是独占一个Surface实例的显示区域,每个窗口的S

  • 深入理解Android中View绘制的三大流程

    前言 最近对Android中View的绘制机制有了一些新的认识,所以想记录下来并分享给大家.View的工作流程主要是指measure.layout.draw这三大流程,即测量.布局和绘制,其中measure确定View的测量宽高,layout根据测量的宽高确定View在其父View中的四个顶点的位置,而draw则将View绘制到屏幕上,这样通过ViewGroup的递归遍历,一个View树就展现在屏幕上了. 说的简单,下面带大家一步一步从源码中分析: Android的View是树形结构的: 基本概

  • Android中View的炸裂特效实现方法详解

    本文实例讲述了Android中View的炸裂特效实现方法.分享给大家供大家参考,具体如下: 前几天微博上被一个很优秀的 Android 开源组件刷屏了 - ExplosionField,效果非常酷炫,有点类似 MIUI 卸载 APP 时的动画,先来感受一下. ExplosionField 不但效果很拉风,代码写得也相当好,让人忍不住要拿来好好读一下. 创建 ExplosionField ExplosionField 继承自 View,在 onDraw 方法中绘制动画特效,并且它提供了一个 att

  • Android中View.post和Handler.post的关系

    目录 前言 为什么要拿这二者来比较? View的渲染起点 View.post的执行流程 Handler.post()能像View.post()一样获取到宽.高数据吗? 前言 View.post和Handler.post是Android开发中经常使用到的两个”post“方法,我们经常通过前者去获取一些View在运行时的渲染数据,或者测量页面的渲染时间.而后者则是Android的核心Handler的一个方法,它会向对应线程的MessageQueue中插入一条Message,在未来的某个事件点得到执行

  • Android 中View.onDraw(Canvas canvas)的使用方法

    Android 中View.onDraw(Canvas canvas)的使用方法 View通过View.onDraw(Canvas canvas)来Draw. 我们可以定义自己的继承于View的TestView,然后重载View.onDraw(Canvas canvas). 对于自定义的TestView如何与Activity关联?有以下两种方式: 直接在setContentView(View view)里面加进去自定义的View:setContentView(new TestView(this)

  • Android中View跟随手指移动效果

    最近做了一个项目中,其中遇到这样的需求要求图片移动到手指触碰的地方.具体实现代码如下所示: package com.example.plane; import Android.app.Activity; import android.os.Bundle; import android.util.DisplayMetrics; import android.view.Display; import android.view.KeyEvent; import android.view.Menu; i

  • Android中View跟随手指滑动效果的实例代码

    本文讲述了Android中View跟随手指滑动效果的实例代码.分享给大家供大家参考,具体如下: 1.android View 主要6种滑动方法,分别是 layout() offsetLeftAndRight()和offsetTopAndBottom() LayoutParams scrollBy()和 scrollTo() Scroller 动画 2.实现效果图 3.自定义中使用layout()方法实习view的滑动 public class MoveView extends View { pr

随机推荐