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

所属分类: 软件编程 / Android 阅读数: 22
收藏 0 赞 0 分享

前言

吸进这几天在看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开发者们能带来一定的帮助,如果有疑问大家可以留言交流。

更多精彩内容其他人还在看

Android实现悬浮窗体效果

这篇文章主要为大家详细介绍了Android实现悬浮窗体效果,显示悬浮窗口,窗口可以拖动,具有一定的参考价值,感兴趣的小伙伴们可以参考一下
收藏 0 赞 0 分享

Andriod studio 打包aar 的方法

这篇文章主要介绍了Andriod studio 打包aar的方法,非常不错,具有一定的参考借鉴价值 ,需要的朋友可以参考下
收藏 0 赞 0 分享

Android加载loading对话框的功能及实例代码(不退出沉浸式效果)

这篇文章主要介绍了Android加载loading对话框的功能及实例代码,不退出沉浸式效果,本文通过实例代码给大家介绍的非常详细,具有一定的参考借鉴价值,需要的朋友可以参考下
收藏 0 赞 0 分享

Android中LayoutInflater.inflater()的正确打开方式

这篇文章主要给大家介绍了关于Android中LayoutInflater.inflater()的正确打开方式,文中通过示例代码介绍的非常详细,需要的朋友可以参考借鉴,下面随着小编来一起学习学习吧
收藏 0 赞 0 分享

Delphi在Android下使用Java库的方法

这篇文章主要介绍了Delphi在Android下使用Java库的方法,本文以Android的USB串口通讯库为例,给大家介绍的非常详细,具有一定的参考借鉴价值,需要的朋友可以参考下
收藏 0 赞 0 分享

Retrofit2日志拦截器的使用

这篇文章主要介绍了Retrofit2日志拦截器的使用,小编觉得挺不错的,现在分享给大家,也给大家做个参考。一起跟随小编过来看看吧
收藏 0 赞 0 分享

Android创建外部lib库及自定义View的图文教程

这篇文章主要给大家介绍了关于Android创建外部lib库及自定义View的相关资料,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
收藏 0 赞 0 分享

Android分享微信小程序失败的一些事小结

这篇文章主要给大家介绍了关于Android分享微信小程序失败一些事,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
收藏 0 赞 0 分享

Android分享微信小程序技巧之图片优化

这篇文章主要给大家介绍了关于Android分享微信小程序技巧之图片优化的相关资料,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
收藏 0 赞 0 分享

Android Viewpager实现无限循环轮播图

这篇文章主要为大家详细介绍了Android Viewpager实现无限循环轮播图,文中示例代码介绍的非常详细,具有一定的参考价值,感兴趣的小伙伴们可以参考一下
收藏 0 赞 0 分享
查看更多