LayoutInflater是如何“移花接木”-下篇

LayoutInflater“移花接木”的上篇,介紹了LayoutInflater對象的獲取方式,更主要的是分析幾種方式的原理,發現最終都是通過獲取系統服務的方式。那麼,本篇算是“移花接木”的重頭,主要分析xml是如何轉換爲view的

獲取LayoutInflater對象後,就是使用Inflate方法,從此開始,揭開“移花接木”之神祕面紗...

LayoutInflater.java

兩個參數,一個xml對應的資源ID,一個是父級的view,也就是你的xml的view要綁定在誰身上

    public View inflate(@LayoutRes int resource, @Nullable ViewGroup root) {
        return inflate(resource, root, root != null);
    }
這裏除上面的兩個參數,多出一個,表示是否綁定在父view上,取決於父view是否爲null

    public View inflate(@LayoutRes int resource, @Nullable ViewGroup root, boolean attachToRoot) {
        final Resources res = getContext().getResources();
        final XmlResourceParser parser = res.getLayout(resource);
        try {
            return inflate(parser, root, attachToRoot);
        } finally {
            parser.close();
        }
    }

此處可以看出,涉及到Resources類、LayoutInflater類,執行以下兩個方法,分析會圍繞這兩個方法展開

步驟1:public XmlResourceParser getLayout(int resourceId)

步驟2:public View inflate(XmlPullParser parser, ViewGroup root, boolean attachToRoot)


步驟1 getLayout分析:

Resources.java

加載xml資源解析器,這裏加載的是layout的,當然,還有anim等資源的解析器,參數type對應的就是“layout”,如果是動畫,對應的是“anim”

    public XmlResourceParser getLayout(@LayoutRes int id) throws NotFoundException {
        return loadXmlResourceParser(id, "layout");
    }

    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");
        }
    }

這裏涉及getValue,loadXmlResource兩個方法,

先看getValue方法內部,是在資源中查找是否存在xml的id,如果有程序繼續執行,如果沒有,則拋出異常,程序中斷

   public void getValue(@AnyRes int id, TypedValue outValue, boolean resolveRefs)
            throws NotFoundException {
        boolean found = mAssets.getResourceValue(id, 0, outValue, resolveRefs);
        if (found) {
            return;
        }
        throw new NotFoundException("Resource ID #0x" + Integer.toHexString(id));
    }

關於異常部分,實際開發中的確遇到過,比如說資源的id寫錯,會拋出資源找不到或者id無效的異常,就是在此校驗

再看loadXmlResource方法,如下:

    XmlResourceParser loadXmlResourceParser(String file, int id, int assetCookie, String type) throws NotFoundException {
        if (id != 0) {
            try {
                // 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;
                        
                        return block.newParser();
                    }
                }
            } 
        }
        throw new NotFoundException(
                "File " + file + " from xml type " + type + " resource ID #0x"
                + Integer.toHexString(id));
    }

乍一看,很多代碼,有點蒙圈,仔細一看,return位置,newParser()方法是重點需要關注的,mCachedXmlBlocks是一個XmlBlock類型數組,如下:

    private final XmlBlock[] mCachedXmlBlocks = new XmlBlock[4];

這樣,最終集中到XmlBlock類,如下:

XmlBlock.java

    public XmlResourceParser newParser() {
        synchronized (this) {
            if (mNative != 0) {
                return new Parser(nativeCreateParseState(mNative), this);
            }
            return null;
        }
    }

看到這,Parser是什麼,Parser是一個類,實現了XmlResourceParser接口,繼承了XmlPullParser接口,

XmlResourceParser雖然是接口,但基本沒聲明方法

也就是說,XmlBlock的內部類Parser,具體實現了XmlPullParser的方法,源碼如下:

    final class Parser implements XmlResourceParser{
        //...
        public String getText() {
       int id = nativeGetText(mParseState);
       return id >= 0 ? mStrings.get(id).toString() : null;
     }
     public int getLineNumber() {
       return nativeGetLineNumber(mParseState);
     }
        //...
        public int next() throws XmlPullParserException,IOException {
            if (!mStarted) {
                mStarted = true;
                return START_DOCUMENT;
            }
            if (mParseState == 0) {
                return END_DOCUMENT;
            }
            int ev = nativeNext(mParseState);
            if (mDecNextDepth) {
                mDepth--;
                mDecNextDepth = false;
            }
            switch (ev) {
            case START_TAG:
                mDepth++;
                break;
            case END_TAG:
                mDecNextDepth = true;
                break;
            }
            mEventType = ev;
            if (ev == END_DOCUMENT) {
                // Automatically close the parse when we reach the end of
                // a document, since the standard XmlPullParser interface
                // doesn't have such an API so most clients will leave us
                // dangling.
                close();
            }
            return ev;

}

Parser的代碼比較繁雜,這裏摘錄了一部分,具體可參見XmlBlock.java源碼

在next()方法中,很清晰看到對於xml步步解析也是使用EventType的進行的,在XmlPullParser.java中TYPES數組定義了xml所有的EventType類型

可以看出,對xml的解析,用到很多native方法,也就是說,xml是靠c代碼解析的,這點避免了java類的編譯過程,尤其是xml解析這種對xml層層解析的過程,是比較耗時的操作,c代碼的執行效率遠高於java代碼

native方法部分具體實現暫時沒有研究,後續慢慢會補上

至此,getLayout分析完

步驟2 Inflate分析開始,

LayoutInflater.java,以下源碼在該類中

第一個參數,步驟1 獲取到的資源解析器

第二個參數,父級的view

第三個參數,是否綁定到父view上

    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];
            mConstructorArgs[0] = inflaterContext;
            View result = root;
                // Look for the root node.
                int type;
                //...
                final String name = parser.getName();
               //...
                // Temp is the root view that was found in the xml
                final View temp = createViewFromTag(root, name, inflaterContext, attrs);
                ViewGroup.LayoutParams params = null;
                //...              
               if (root != null) {
                        // Create layout params that match root, if supplied
                        params = 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);
                        }
                    }
                    //...
                    // 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;
                    }
                    //...
            return result;
        }
    }

上面代碼大致過程:

首先,執行通過解析器,獲取到要inflate的view名稱,

然後,調用createViewFromTag,參數依次,父view,view名稱,上下文,view對應的AttributeSet,最終得到temp,也就是我們inflate xml佈局後的view,

值得注意的,這個temp是xml佈局中最頂的,比如,xml佈局中最頂級是Framlayout,那麼,temp對應的就是Framlayout,如果是LinearLayout,temp對應的就是LinearLayout,這也是爲什麼我們finViewById時,需要指定view.findViewById的原因,從頂級父view開始搜尋指定id的子view

接着,通過父view得到LayoutParams,設置view的LayoutParams,將view綁定到父view上

如果沒有父view,直接返回view

接下來,具體看createViewFromTag,代碼執行如下:

    private View createViewFromTag(View parent, String name, Context context, AttributeSet attrs) {
        return createViewFromTag(parent, name, context, attrs, false);
    }

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();
        }
        //...
        try {
            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;
        } 
    }

看完這段代碼,是不是很爽?基本不大明白,別怕,這裏我們只關注需要的,那就是最終返回的view,

上面的關鍵執行過程:mFactory2,mFactory,mPrivateFactory的判斷邏輯,不爲null,分別回調各自方法賦值給view,全部爲null,調用類方法createView,onCreateView

涉及到對應接口mFactory2,mFactory,mPrivateFactory對應的方法,有類方法createView,onCreateView,也有回調方法onCreateView

先看接口mFactory2,mFactory,mPrivateFactory,是在LayoutInfalter的構造中初始化的,如下:

    protected LayoutInflater(LayoutInflater original, Context newContext) {
        mContext = newContext;
        mFactory = original.mFactory;
        mFactory2 = original.mFactory2;
        mPrivateFactory = original.mPrivateFactory;
        setFilter(original.mFilter);
    }
關於上面LayoutInfalter的構造方法,沒有找到具體在哪被實例化,不過,這並不影響我們分析,這裏認定mFactory2,mFactory,mPrivateFactory全部爲null,那麼,按照上面

代碼邏輯,就會調用LayoutInflater的類方法createView,onCreateView,代碼如下:

    protected View onCreateView(String name, AttributeSet attrs)
            throws ClassNotFoundException {
        return createView(name, "android.view.", attrs);
    }

onCreateView內部執行的是createView方法,如下:

第一個參數,view的名稱,第二個參數view對應的類名前綴,第三個參數view對應的AttributeSet

public final View createView(String name, String prefix, AttributeSet attrs){
        Constructor<? extends View> constructor = sConstructorMap.get(name);
        Class<? extends View> clazz = null;
        //...
            if (constructor == 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);
                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 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[] 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]));
            }
            return view;
            //...
    }

代碼很長,一點點分析,首先是從sConstructorMap中取對應name的view構造,也就是從緩存中取構造,

如果存在構造,則執行else分支,如果沒有構造,則執行if,獲取字節碼對象,獲取構造,添加到sConstructorMap緩存中,方便再次使用

獲取view類的構造方法後,創建view的實例對象,返回實例對象

這個過程是反射的運用

以上是假定,mFactory2,mFactory,mPrivateFactory全部爲null情況

另外,關於mFactory2,mFactory,mPrivateFactory全部爲null,在LayoutInflater"移花接木"-上篇中,提到,三種獲取LayoutInflater方式,最終獲取到的是SystemServiceRegistry類中註冊的PhoneLayoutInflater的實例,構造函數以及父類構造函數,如下:

    public PhoneLayoutInflater(Context context) {
        super(context);
    }
    protected LayoutInflater(Context context) {
        mContext = context;
    }

這也說明,創建LayoutInfalter並未涉及到,protected LayoutInflater(LayoutInflater original, Context newContext),這種構造方法,那麼,mFactory2,mFactory,mPrivateFactory自然也就全部爲null

Inflate分析完成

步驟1 步驟2,整個Inflater.inflate方法執行過程分析到此結束


總結:xml佈局,資源文件的解析,底層是用c代碼編寫的,執行效率高

            xml佈局轉化爲view最終是採用反射的方式,獲取實例對象的





發佈了41 篇原創文章 · 獲贊 18 · 訪問量 12萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章