学习是一个过程。

onCreate加载布局,是不是都很熟悉。
@Overrideprotected void onCreate(@Nullable @org.jetbrains.annotations.Nullable Bundle savedInstanceState) {super.onCreate(savedInstanceState);setContentView(R.layout.activity_happy);}
AppCompatActivity的onCreate()方法,注意这不是Activity的onCreate方法。
@Overrideprotected void onCreate(@Nullable Bundle savedInstanceState) {final AppCompatDelegate delegate = getDelegate();//注意此方法是做换肤的关键。delegate.installViewFactory();delegate.onCreate(savedInstanceState);super.onCreate(savedInstanceState);} AppCompatDelegatelmpl的installViewFactory方法,AppCompatDelegatelmpl实现了Factory2接口。
public void installViewFactory() {LayoutInflater layoutInflater = LayoutInflater.from(mContext);if (layoutInflater.getFactory() == null) {//AppCompatDelegatelmpl实现了Factory2接口LayoutInflaterCompat.setFactory2(layoutInflater, this);} else {if (!(layoutInflater.getFactory2() instanceof AppCompatDelegateImpl)) {Log.i(TAG, "The Activity's LayoutInflater already has a Factory installed"+ " so we can not install AppCompat's");}}}
LayoutInflater中的setFactory2方法,此方法不允许重复设置值,如果设置值会产生异常,所以如果做动态换肤设置Factory2时,要放在super.onCreate()方法之前,防止异常退出。
public void setFactory2(Factory2 factory) {//从这可以看出factory是不可以重复设置值的,如果重复设置会产生异常。if (mFactorySet) {throw new IllegalStateException("A factory has already been set on this LayoutInflater");}if (factory == null) {throw new NullPointerException("Given factory can not be null");}mFactorySet = true;//mFactory与mFactory2一块赋值,mFractory2是按照扩展的方法进行开发的。if (mFactory == null) {mFactory = mFactory2 = factory;} else {mFactory = mFactory2 = new FactoryMerger(factory, factory, mFactory, mFactory2);}}
继续分析setContentView()方法,AppCompatActivity中的setContentView调用的是AppCompatDelegatelmpl的方法。
@Overridepublic void setContentView(@LayoutRes int layoutResID) {getDelegate().setContentView(layoutResID);} AppCompatDelegatelmpl的setContentView方法,此方法主要是加载我们自定义的布局,将布局添加到容器中。
public void setContentView(int resId) {//主要是初始化根布局,用来存放我们自定义的布局。ensureSubDecor();//存放我们自定义布局的View,此View的类型是FrameLayoutViewGroup contentParent = mSubDecor.findViewById(android.R.id.content);contentParent.removeAllViews();//加载自定义布局,将布局添加到contentParent中。LayoutInflater.from(mContext).inflate(resId, contentParent);mAppCompatWindowCallback.getWrapped().onContentChanged();} LayoutInflater的inflate方法,此方法中有以后插件化用到的关键代码,此处先留意一下,以后有机会再进行分享插件化相关的技术。
public View inflate(@LayoutRes int resource, @Nullable ViewGroup root, boolean attachToRoot) {//此处是做插件化的关键,activity自定义getResources()方法,用来生产插件对应的资源。final Resources res = getContext().getResources();if (DEBUG) {Log.d(TAG, "INFLATING from resource: \"" + res.getResourceName(resource) + "\" ("+ Integer.toHexString(resource) + ")");}final XmlResourceParser parser = res.getLayout(resource);try {//继续分析return inflate(parser, root, attachToRoot);} finally {parser.close();}} 继续分析inflate方法,其重要流程是创建根布局,然后创建
public View inflate(XmlPullParser parser, @Nullable ViewGroup root, boolean attachToRoot) {synchronized (mConstructorArgs) {Trace.traceBegin(Trace.TRACE_TAG_VIEW, "inflate");final Context inflaterContext = mContext;final AttributeSet attrs = Xml.asAttributeSet(parser);Context lastContext = (Context) mConstructorArgs[0];mConstructorArgs[0] = inflaterContext;View result = root;try {// Look for the root node.int type;while ((type = parser.next()) != XmlPullParser.START_TAG &&type != XmlPullParser.END_DOCUMENT) {// Empty}if (type != XmlPullParser.START_TAG) {throw new InflateException(parser.getPositionDescription()+ ": No start tag found!");}final String name = parser.getName();if (DEBUG) {System.out.println("**************************");System.out.println("Creating root view: "+ name);System.out.println("**************************");}//处理 merge 标签if (TAG_MERGE.equals(name)) {if (root == null || !attachToRoot) {throw new InflateException(" can be used only with a valid "+ "ViewGroup root and attachToRoot=true");}rInflate(parser, root, inflaterContext, attrs, false);} else {// Temp is the root view that was found in the xml//自定义View的根布局,就是自己写的布局的根布局。final View temp = createViewFromTag(root, name, inflaterContext, attrs);ViewGroup.LayoutParams params = null;if (root != null) {if (DEBUG) {System.out.println("Creating params from root: " +root);}// Create layout params that match root, if suppliedparams = root.generateLayoutParams(attrs);if (!attachToRoot) {// Set the layout params for temp if we are not// attaching. (If we are, we use addView, below)temp.setLayoutParams(params);}}if (DEBUG) {System.out.println("-----> start inflating children");}// Inflate all children under temp against its context.//把自定义的xml所有除根布局之外的控件全部实例化然后添加进根布局rInflateChildren(parser, temp, attrs, true);if (DEBUG) {System.out.println("-----> done inflating children");}// 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);}// Decide whether to return the root that was passed in or the// top view found in xml.if (root == null || !attachToRoot) {result = temp;}}} catch (XmlPullParserException e) {final InflateException ie = new InflateException(e.getMessage(), e);ie.setStackTrace(EMPTY_STACK_TRACE);throw ie;} catch (Exception e) {final InflateException ie = new InflateException(parser.getPositionDescription()+ ": " + e.getMessage(), e);ie.setStackTrace(EMPTY_STACK_TRACE);throw ie;} finally {// Don't retain static reference on context.mConstructorArgs[0] = lastContext;mConstructorArgs[1] = null;Trace.traceEnd(Trace.TRACE_TAG_VIEW);}return result;}}
LayoutInflater的createViewFromTag方法,注意这里有一个BlinkLayout的闪烁小彩蛋,用来闪烁布局。
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();}``//闪烁的菜单,是为了庆祝??? 1995年庆祝什么节日?if (name.equals(TAG_1995)) {// Let's party like it's 1995!return new BlinkLayout(context, attrs);}try {View view;//如果是AppCompatActivity在这初始化,这个可以自己创建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);}//如果是Activity在这初始化。if (view == null) {final Object lastContext = mConstructorArgs[0];mConstructorArgs[0] = context;try {//不带包路径的View,最终都会调用到createView的这个方法if (-1 == name.indexOf('.')) {//这个最终会调用createView(name,“android.view.”,attrs),携带android.view前缀。view = onCreateView(parent, name, attrs);} else {view = createView(name, null, attrs);}} finally {mConstructorArgs[0] = lastContext;}}return view;} catch (InflateException e) {throw e;} catch (ClassNotFoundException e) {final InflateException ie = new InflateException(attrs.getPositionDescription()+ ": Error inflating class " + name, e);ie.setStackTrace(EMPTY_STACK_TRACE);throw ie;} catch (Exception e) {final InflateException ie = new InflateException(attrs.getPositionDescription()+ ": Error inflating class " + name, e);ie.setStackTrace(EMPTY_STACK_TRACE);throw ie;}}
我们跟踪一下Layoutlnflater的createView方法,
//利用反射创建对象,为啥不直接new对象呢?因为有些不能访问到?
public final View createView(String name, String prefix, AttributeSet attrs)throws ClassNotFoundException, InflateException {Constructor extends View> constructor = sConstructorMap.get(name);//鉴别构造方法是否失效,主要为了鉴别类加载器。if (constructor != null && !verifyClassLoader(constructor)) {constructor = null;sConstructorMap.remove(name);}Class extends View> clazz = null;try {Trace.traceBegin(Trace.TRACE_TAG_VIEW, name);if (constructor == null) {// Class not found in the cache, see if it's real, and try to add itclazz = mContext.getClassLoader().loadClass(prefix != null ? (prefix + name) : name).asSubclass(View.class);if (mFilter != null && clazz != null) {boolean allowed = mFilter.onLoadClass(clazz);if (!allowed) {failNotAllowed(name, prefix, attrs);}}constructor = clazz.getConstructor(mConstructorSignature);constructor.setAccessible(true);//为了增加效率,增加了缓存。sConstructorMap.put(name, constructor);} else {// If we have a filter, apply it to cached constructorif (mFilter != null) {// Have we seen this name before?Boolean allowedState = mFilterMap.get(name);if (allowedState == null) {// New class -- remember whether it is allowedclazz = mContext.getClassLoader().loadClass(prefix != null ? (prefix + name) : name).asSubclass(View.class);boolean allowed = clazz != null && mFilter.onLoadClass(clazz);mFilterMap.put(name, allowed);if (!allowed) {failNotAllowed(name, prefix, attrs);}} else if (allowedState.equals(Boolean.FALSE)) {failNotAllowed(name, prefix, attrs);}}}Object lastContext = mConstructorArgs[0];if (mConstructorArgs[0] == null) {// Fill in the context if not already within inflation.mConstructorArgs[0] = mContext;}Object[] args = mConstructorArgs;args[1] = attrs;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]));}mConstructorArgs[0] = lastContext;return view;} catch (NoSuchMethodException e) {final InflateException ie = new InflateException(attrs.getPositionDescription()+ ": Error inflating class " + (prefix != null ? (prefix + name) : name), e);ie.setStackTrace(EMPTY_STACK_TRACE);throw ie;} catch (ClassCastException e) {// If loaded class is not a View subclassfinal InflateException ie = new InflateException(attrs.getPositionDescription()+ ": Class is not a View " + (prefix != null ? (prefix + name) : name), e);ie.setStackTrace(EMPTY_STACK_TRACE);throw ie;} catch (ClassNotFoundException e) {// If loadClass fails, we should propagate the exception.throw e;} catch (Exception e) {final InflateException ie = new InflateException(attrs.getPositionDescription() + ": Error inflating class "+ (clazz == null ? "" : clazz.getName()), e);ie.setStackTrace(EMPTY_STACK_TRACE);throw ie;} finally {Trace.traceEnd(Trace.TRACE_TAG_VIEW);}} 继续分析mFactory2.onCreateView()的方法,其最终会调用到AppCompatDelegatelmpl的createView(),此函数主要对mAppCompatViewInflater进行初始化,然后调用其createView()方法。
public View createView(View parent, final String name, @NonNull Context context,@NonNull AttributeSet attrs) {if (mAppCompatViewInflater == null) {TypedArray a = mContext.obtainStyledAttributes(R.styleable.AppCompatTheme);String viewInflaterClassName =a.getString(R.styleable.AppCompatTheme_viewInflaterClass);if ((viewInflaterClassName == null)|| AppCompatViewInflater.class.getName().equals(viewInflaterClassName)) {// Either default class name or set explicitly to null. In both cases// create the base inflater (no reflection)mAppCompatViewInflater = new AppCompatViewInflater();} else {try {Class> viewInflaterClass = Class.forName(viewInflaterClassName);mAppCompatViewInflater =(AppCompatViewInflater) viewInflaterClass.getDeclaredConstructor().newInstance();} catch (Throwable t) {Log.i(TAG, "Failed to instantiate custom view inflater "+ viewInflaterClassName + ". Falling back to default.", t);mAppCompatViewInflater = new AppCompatViewInflater();}}}boolean inheritContext = false;if (IS_PRE_LOLLIPOP) {inheritContext = (attrs instanceof XmlPullParser)// If we have a XmlPullParser, we can detect where we are in the layout? ((XmlPullParser) attrs).getDepth() > 1// Otherwise we have to use the old heuristic: shouldInheritContext((ViewParent) parent);}return mAppCompatViewInflater.createView(parent, name, context, attrs, inheritContext,IS_PRE_LOLLIPOP, /* Only read android:theme pre-L (L+ handles this anyway) */true, /* Read read app:theme as a fallback at all times for legacy reasons */VectorEnabledTintResources.shouldBeUsed() /* Only tint wrap the context if enabled */);} AppCompatViewInflater的createView()来创建View,其最终都转换为AppCompat对应的组件,
final View createView(View parent, final String name, @NonNull Context context,@NonNull AttributeSet attrs, boolean inheritContext,boolean readAndroidTheme, boolean readAppTheme, boolean wrapContext) {final Context originalContext = context;// We can emulate Lollipop's android:theme attribute propagating down the view hierarchy// by using the parent's contextif (inheritContext && parent != null) {context = parent.getContext();}if (readAndroidTheme || readAppTheme) {// We then apply the theme on the context, if specifiedcontext = themifyContext(context, attrs, readAndroidTheme, readAppTheme);}if (wrapContext) {context = TintContextWrapper.wrap(context);}View view = null;// We need to 'inject' our tint aware Views in place of the standard framework versions//这里对View进行转换,自动转换为AppCompat对应的View。switch (name) {case "TextView":view = createTextView(context, attrs);verifyNotNull(view, name);break;case "ImageView":view = createImageView(context, attrs);verifyNotNull(view, name);break;case "Button":view = createButton(context, attrs);verifyNotNull(view, name);break;case "EditText":view = createEditText(context, attrs);verifyNotNull(view, name);break;case "Spinner":view = createSpinner(context, attrs);verifyNotNull(view, name);break;case "ImageButton":view = createImageButton(context, attrs);verifyNotNull(view, name);break;case "CheckBox":view = createCheckBox(context, attrs);verifyNotNull(view, name);break;case "RadioButton":view = createRadioButton(context, attrs);verifyNotNull(view, name);break;case "CheckedTextView":view = createCheckedTextView(context, attrs);verifyNotNull(view, name);break;case "AutoCompleteTextView":view = createAutoCompleteTextView(context, attrs);verifyNotNull(view, name);break;case "MultiAutoCompleteTextView":view = createMultiAutoCompleteTextView(context, attrs);verifyNotNull(view, name);break;case "RatingBar":view = createRatingBar(context, attrs);verifyNotNull(view, name);break;case "SeekBar":view = createSeekBar(context, attrs);verifyNotNull(view, name);break;case "ToggleButton":view = createToggleButton(context, attrs);verifyNotNull(view, name);break;default:// The fallback that allows extending class to take over view inflation// for other tags. Note that we don't check that the result is not-null.// That allows the custom inflater path to fall back on the default one// later in this method.view = createView(context, name, attrs);}//如果不是以上对应的View,则调用以下方法进行创建。if (view == null && originalContext != context) {// If the original context does not equal our themed context, then we need to manually// inflate it using the name so that android:theme takes effect.view = createViewFromTag(context, name, attrs);}if (view != null) {// If we have created a view, check its android:onClickcheckOnClickListener(view, attrs);}return view;}
AppCompatViewInflater的createViewFromTag()方法,其与LayoutInflater的createViewFromTag方法有些类似,
private View createViewFromTag(Context context, String name, AttributeSet attrs) {if (name.equals("view")) {name = attrs.getAttributeValue(null, "class");}try {mConstructorArgs[0] = context;mConstructorArgs[1] = attrs;if (-1 == name.indexOf('.')) {//尝试增加不同的前缀进行创建,用反射,如果 反射失败则加载不成功,再重新尝试。for (int i = 0; i < sClassPrefixList.length; i++) {final View view = createViewByPrefix(context, name, sClassPrefixList[i]);if (view != null) {return view;}}return null;} else {return createViewByPrefix(context, name, null);}} catch (Exception e) {// We do not want to catch these, lets return null and let the actual LayoutInflater// tryreturn null;} finally {// Don't retain references on context.mConstructorArgs[0] = null;mConstructorArgs[1] = null;}} 尝试用不同的前缀反射创建View,到了这里View就创建成功了。
private View createViewByPrefix(Context context, String name, String prefix)throws ClassNotFoundException, InflateException {Constructor extends View> constructor = sConstructorMap.get(name);try {if (constructor == null) {// Class not found in the cache, see if it's real, and try to add itClass extends View> clazz = Class.forName(prefix != null ? (prefix + name) : name,false,context.getClassLoader()).asSubclass(View.class);constructor = clazz.getConstructor(sConstructorSignature);//为了增加速度,这里也尝试了缓存的方式。sConstructorMap.put(name, constructor);}constructor.setAccessible(true);return constructor.newInstance(mConstructorArgs);} catch (Exception e) {// We do not want to catch these, lets return null and let the actual LayoutInflater// tryreturn null;}}
在onCreateView中设置Factory2,通过回调函数我们能拿到View的名称(注意,我们连系统的根布局的名称也是可以拿到的),同事也可以拿到其对应的属性值,这样我们就可以根据这些值来创建我们的View,在这里也可以动态的设置我们想要的皮肤。
@Overrideprotected void onCreate(@Nullable @org.jetbrains.annotations.Nullable Bundle savedInstanceState) {getLayoutInflater().setFactory2(new LayoutInflater.Factory2() {//这里主要负责View的创建。@Overridepublic View onCreateView(View parent, String name, Context context, AttributeSet attrs) {Log.e(TAG, "name: " + name);int attributeCount = attrs.getAttributeCount();for (int i = 0; i < attributeCount; i++) {String attributeName = attrs.getAttributeName(i);Log.e(TAG, "attributeName: " + attributeName);}Log.e(TAG, "------------------------------------divide line------------------- ");return null;}@Overridepublic View onCreateView(String name, Context context, AttributeSet attrs) {return null;}});super.onCreate(savedInstanceState);setContentView(R.layout.activity_happy);}
日志打印:
name: LinearLayout
attributeName: orientation
attributeName: fitsSystemWindows
attributeName: layout_width
attributeName: layout_height
------------------------------------divide line-------------------
name: ViewStub
attributeName: theme
attributeName: id
attributeName: layout
attributeName: inflatedId
attributeName: layout_width
attributeName: layout_height
------------------------------------divide line-------------------
name: FrameLayout
attributeName: id
attributeName: layout_width
attributeName: layout_height
attributeName: foreground
attributeName: foregroundGravity
attributeName: foregroundInsidePadding
------------------------------------divide line-------------------
name: androidx.appcompat.widget.FitWindowsLinearLayout
attributeName: orientation
attributeName: id
attributeName: fitsSystemWindows
attributeName: layout_width
attributeName: layout_height
------------------------------------divide line-------------------
name: androidx.appcompat.widget.ViewStubCompat
attributeName: id
attributeName: layout
attributeName: inflatedId
attributeName: layout_width
attributeName: layout_height
------------------------------------divide line-------------------
name: androidx.appcompat.widget.ContentFrameLayout
attributeName: id
attributeName: layout_width
attributeName: layout_height
attributeName: foreground
attributeName: foregroundGravity
------------------------------------divide line-------------------
name: LinearLayout
attributeName: orientation
attributeName: layout_width
attributeName: layout_height
------------------------------------divide line-------------------
我们可以以apk的方式来打包资源,这个apk资源包中只存在资源文件,然后我们通过动态加载技术来加载apk资源包,实现动态换肤的功能。
下一篇:休多问,恨深