源碼學習《3》Layout.xml 的解析和 xml 標籤生成 View 對象的過程(App 換膚原理 1)

今天要學習源碼的兩個問題:

  1. Layout.xml佈局是怎麼加載解析的
  2. Layout.xml中的 view 標籤又是怎麼被轉化成對象的

針對這兩個問題引出源碼學習的流程,帶着問題去看源碼。

總體流程:

問題 1 佈局是怎麼加載的 :

首先我們最熟悉的代碼肯定是:

public class MainActivity extends AppCompatActivity {

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

那就讓我們從 setContentView(R.layout.activity_main); 入手。進入方法發現:

  @Override
    public void setContentView(int resId) {
        ensureSubDecor();
        ViewGroup contentParent = (ViewGroup) mSubDecor.findViewById(android.R.id.content);
        contentParent.removeAllViews();
        LayoutInflater.from(mContext).inflate(resId, contentParent);
        mOriginalWindowCallback.onContentChanged();
    }

進入到 AppCompatDelegateImplV9 這是個爲委託類,不是今天的重點,setContentView(int resId) 中地一句就是 ensureSubDecor(); 這個方法,由名字可知,創建sub decor view對象的。進入該方法 繼續~

   private void ensureSubDecor() {
        if (!mSubDecorInstalled) {
            // 創建 sub decor
            mSubDecor = createSubDecor();

            applyFixedSizeWindow();
            ...省略...
            }
        }
    }

首先會進入 createSubDecor() 創建對象 繼續 ~

private ViewGroup createSubDecor() {
        
        ..判斷主題..省略...
        // Now let's make sure that the Window has installed its decor by retrieving it
        mWindow.getDecorView();

        final LayoutInflater inflater = LayoutInflater.from(mContext);
        ViewGroup subDecor = null;

        ....省略..根據條件傳教 subdecor.......
                // Now inflate the view using the themed context and set it as the content view
                subDecor = (ViewGroup) LayoutInflater.from(themedContext)
                        .inflate(R.layout.abc_screen_toolbar, null);

                mDecorContentParent = (DecorContentParent) subDecor
                        .findViewById(R.id.decor_content_parent);
                mDecorContentParent.setWindowCallback(getWindowCallback());

               

        // Now set the Window's content view with the decor
        mWindow.setContentView(subDecor);

      ...省略....

        return subDecor;
    }

首先是設置主題,然後 mWindow.getDecorView(); 確保decor view 已經創建,接下來根據不同的條件創建不同的 sub decor ,subdecor其實是一個 toolbar + framelayout 佈局,content_parent 其實就是我們自己佈局的父佈局。                                             接下里就是 mWindow.setContentView(subDecor); 把subdecor傳給window中的decorview。

先看 mWindow.getDecorView();

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 {
            mDecor.setWindow(this);
        }
        if (mContentParent == null) {
            // 創建 decorview 內部的 contentparent佈局對象
            mContentParent = generateLayout(mDecor);
              ...省略...

到這裏首先是通過generateDecor(-1); 創建decorview的對象,然後在generateLayout(mDecor); 創建 內部的contentparent 對象。

然後decorView和contentparent對象創建完成之後,使用 mWindow.setContentView(subDecor);  把subdecor 對象addview放入contentparent中,代碼

public void setContentView(View view, ViewGroup.LayoutParams params) {
        // 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 {
            mContentParent.addView(view, params);
        }
        ...省略..
    }
setContentView(其實就是把我們創建的subdecor 對象add進入 DecorVIew中的 contentparent中去。

然後代碼又回到 setContentView(int resId) 

    @Override
    public void setContentView(int resId) {
        ensureSubDecor();
        ViewGroup contentParent = (ViewGroup) mSubDecor.findViewById(android.R.id.content);
        contentParent.removeAllViews();
        LayoutInflater.from(mContext).inflate(resId, contentParent);
        mOriginalWindowCallback.onContentChanged();
    }

這時候的mSubDecor對象已經被我們創建並且被放入window 的 decorview中去。然後把我們的佈局 給 加入到 subdecor中。

問題1 我們就有答案了

接下來看問題 2 ,我們佈局被加載到 系統的decorview中 後是怎麼被實例化的...通過上面的代碼我們能看到

LayoutInflater.from(mContext).inflate(resId, contentParent);

我們的佈局被傳進去了,接下來就跟蹤 layoutinflater。

問題 2 加載了我們的佈局xml之後, tag標籤又是怎麼被創建成對象的呢?

通過上述我們知道我們要跟蹤 LayoutInflater 的 inflate() 方法。

    public View inflate(@LayoutRes int resource, @Nullable ViewGroup root, boolean attachToRoot) {
        final Resources res = getContext().getResources();
        // 把 layoutRes 轉換成 parser 解析器
        final XmlResourceParser parser = res.getLayout(resource);
        try {
            return inflate(parser, root, attachToRoot);
        } finally {
            parser.close();
        }
    }

首先就是根據layout id 獲取對應的parser解析器 res.getLayout(resource); 然後 inflate(parser, root, attachToRoot); 解析佈局。

先看下 res.getLayout(resource); 最後會調到 loadXmlResourceParser() 中:

    XmlResourceParser loadXmlResourceParser(@AnyRes int id, @NonNull String type)
            throws NotFoundException {
          // 得到 TypedValue 實例
        final TypedValue value = obtainTempTypedValue();
        try {
            final ResourcesImpl impl = mResourcesImpl;
            // 根據 layout id 把佈局信息 賦值給 TypedValue 對象 
            impl.getValue(id, value, true);
            if (value.type == TypedValue.TYPE_STRING) {
                // 根據 layout 信息 返回該佈局的 parser 解析器
                return impl.loadXmlResourceParser(value.string.toString(), id,
                        value.assetCookie, type);
            }
    .....省略......
    }

通過 impl.getValue(id, value, true); 根據 layout id 把layout 佈局信息賦值給 typedvalue 對象,最終調到 native 方法中。

    final boolean getResourceValue(@AnyRes int resId, int densityDpi, @NonNull TypedValue outValue,
            boolean resolveRefs) {
        synchronized (this) {
            final int block = loadResourceValue(resId, (short) densityDpi, outValue, resolveRefs);
       ........省略...........
        }

最後調用下述方法把 parser 返回去

                return impl.loadXmlResourceParser(value.string.toString(), id,
                        value.assetCookie, type);

這裏就是根據被賦值的 value 對象創建對應layout id 的 parser。

最後又回到了 LayoutInflater的  inflate() 方法中 

public View inflate(XmlPullParser parser, @Nullable ViewGroup root, boolean attachToRoot) {
        
              ........省略.............
                final String name = parser.getName();

                if (DEBUG) {
                    System.out.println("**************************");
                    System.out.println("Creating root view: "
                            + name);
                    System.out.println("**************************");
                }

                if (TAG_MERGE.equals(name)) {
                    if (root == null || !attachToRoot) {
                        throw new InflateException("<merge /> 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
                    // 根據 tag name 創建對應的 view 對象
                    final View temp = createViewFromTag(root, name, inflaterContext, attrs);

                   ......省略.....

            return result;
        }
    }

通過上述代碼我們知道 <merge /> 和 <include /> 是不能作爲 root 根佈局的。

然後就是  final View temp = createViewFromTag(root, name, inflaterContext, attrs);  創建對象

 View createViewFromTag(View parent, String name, Context context, AttributeSet attrs,
            boolean ignoreThemeAttr) {
       
        try {
            View view;
            if (mFactory2 != null) {
                // 回調創建 view 對象
                view = mFactory2.onCreateView(parent, name, context, attrs);
            } else if (mFactory != null) {
                view = mFactory.onCreateView(name, context, attrs);
            } else {
                view = null;
            }


            if (view == null) {
                final Object lastContext = mConstructorArgs[0];
                mConstructorArgs[0] = context;
                try {
                    if (-1 == name.indexOf('.')) {
                       // 創建 view 對象,如果爲空
                        view = onCreateView(parent, name, attrs);
                    } else {
                        view = createView(name, null, attrs);
                    }
                } finally {
                    mConstructorArgs[0] = lastContext;
                }
            }

            return view;
        ..........省略......
    }

然後就是通過 

view = mFactory2.onCreateView(parent, name, context, attrs); 回調創建對象,這個回調其實 是 AppCompatDelegateImplV9 中的 onCreateView(parent, name, context, attrs) 方法

這個方法待會說,如果這個方法返回來的view 依然是 null 就會走到下面的判斷

  if (-1 == name.indexOf('.')) {
                        view = onCreateView(parent, name, attrs);
                    } else {
                        view = createView(name, null, attrs);
                    }

這個方法中會根據 當前 view 是否包含 “.” 來 通過 反射創建 view 對象。

 public final View createView(String name, String prefix, AttributeSet attrs)
            throws ClassNotFoundException, InflateException {
        Constructor<? extends View> constructor = sConstructorMap.get(name);
        ........省略..........
            } else {
                // If we have a filter, apply it to cached constructor
                if (mFilter != null) {
                    // Have we seen this name before?
                    Boolean allowedState = mFilterMap.get(name);
                    if (allowedState == null) {
                        // New class -- remember whether it is allowed
                        clazz = 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;

            // 反射創建 view 對象
            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;
    }

回過頭 讓我們繼續分析 AppCompatDelegateImplV9 中的 onCreateView(parent, name, context, attrs) 方法。

    @Override
    public final View onCreateView(View parent, String name, Context context, AttributeSet attrs) {
        // First let the Activity's Factory try and inflate the view
        final View view = callActivityOnCreateView(parent, name, context, attrs);
        if (view != null) {
            return view;
        }

        // If the Factory didn't handle it, let our createView() method try
        return createView(parent, name, context, attrs);
    }

這個方法最後會走到 createView(parent, name, context, attrs);

    @Override
    public View createView(View parent, final String name, @NonNull Context context,
            @NonNull AttributeSet attrs) {

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

            .........省略.....

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

這個方法有兩個地方1是創建了 AppCompatViewInflater 對象,2 調用了 AppCompatViewInflater 對象的 createView 方法

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 context
        

        View view = null;

        // We need to 'inject' our tint aware Views in place of the standard framework versions
        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;
            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);
        }

        return view;
    }

根據這個方法可以看到,他會根據 name 判斷當前是那個 view 然後 調用createxxx()方法創建 view,其中一個例子:

    @NonNull
    protected AppCompatTextView createTextView(Context context, AttributeSet attrs) {
        return new AppCompatTextView(context, attrs);
    }

可以看到這裏是 new 出來的,AppCOmpat XXX view 是谷歌做的兼容。

基本上整個流程就結束了,我們看到了 我們的view 創建的地方。

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章