安卓 - 源碼 - LayoutInflater(二)

`上一節:安卓 - 源碼分析 - LayoutInflater(一)

inflate方法的重載:

inflate方法有很多重載,而最終將會進入:

/*
 * 3個參數分別為:
 * parser       : 已經包含xml布局的xml解析器
 * root         : 可選參數,生成的布局的根視圖,作用取決于attachToRoot參數
 * attachToRoot : 當為true時,生成的View作為子控件添加到根視圖root;
                  當為false時,root僅為生成的View提供外部的LayoutParams;
 */
public View inflate(XmlPullParser parser, ViewGroup root, boolean attachToRoot)

當調用參數為xml資源id的重載方法時:

public View inflate(@LayoutRes int resource, @Nullable ViewGroup root, boolean attachToRoot)

則會使用getResources().getLayout(resource)將xml布局資源轉換為XmlResourceParser:

final XmlResourceParser parser = res.getLayout(resource);
try {
    return inflate(parser, root, attachToRoot);
} finally {
    parser.close();
}

默認的attachToRoot由root參數是否null決定:

public View inflate(XmlPullParser parser, @Nullable ViewGroup root){
     return inflate(parser, root, root != null);
}
inflate的過程:

解析過程,只需要關注最終使用的inflate方法。去掉Debug和Trace,取出關鍵的源碼先大概了解:

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;

        try {
            int type;

            //Look for the root node.
            while ((type = parser.next()) != XmlPullParser.START_TAG &&
                    type != XmlPullParser.END_DOCUMENT) {
            }
            if (type != XmlPullParser.START_TAG) {
                throw new InflateException(parser.getPositionDescription()
                        + ": No start tag found!");
            }

            final String name = parser.getName();

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

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

                // Decide whether to return the root that was passed in or the
                // top view found in xml.
                if (root == null || !attachToRoot) {
                    result = temp;
                }
            }
        } catch (Exception e) {
            ...
        } finally {
            // Don't retain static reference on context.
            mConstructorArgs[0] = lastContext;
            mConstructorArgs[1] = null;
        }
        return result;
    }
}

應該已經很明顯了,然后我們一步步分析:

/*
 * 這里主要做了一些關鍵對象的配置和臨時保存。
 */
final Context inflaterContext = mContext;
final AttributeSet attrs = Xml.asAttributeSet(parser);
Context lastContext = (Context) mConstructorArgs[0];
mConstructorArgs[0] = inflaterContext;
View result = root;
/*
 * 這里主要檢索xml是否能夠被解析。
 * 查找第一個標簽,直到文件結束。
 * 一個標簽都找不到時,拋出異常,結束解析。
 */
while ((type = parser.next()) != XmlPullParser.START_TAG &&
    type != XmlPullParser.END_DOCUMENT) {
}
if (type != XmlPullParser.START_TAG) {
    throw new InflateException(parser.getPositionDescription()
            + ": No start tag found!");
}
/*
 * 這里主要是為xml布局文件創建一個根節點,
 */
if (TAG_MERGE.equals(name)) {

    /*
     * 當標簽是merge時,必須有父布局及需要添加到父布局,否則異常
     */
    if (root == null || !attachToRoot) {
        throw new InflateException("<merge /> can be used only with a valid "
            + "ViewGroup root and attachToRoot=true");
    }

    /*
     * 當第一個標簽是<merge/>時,調用rInflate方法解析xml并填充到參數給出的root
     * rInflate方法后面介紹
     */
    rInflate(parser, root, inflaterContext, attrs, false);
} else {

    /*
     * 當第一個標簽不是merge時,使用這個標簽創建一個根節點temp,
     * 調用createViewFromTag方法創建View,這個方法后面介紹
     */
    final String name = parser.getName();
    final View temp = createViewFromTag(root, name, inflaterContext, attrs);

    /* 這里的流程下面介紹 */
    ...
}
/*
 * 根據root和attachToRoot,為根節點temp配置合適的LayoutParams。
 */
ViewGroup.LayoutParams params = null;
if (root != null) {
    params = root.generateLayoutParams(attrs);
    if (!attachToRoot) {
        temp.setLayoutParams(params);
    }
}
/*
 * 通過rInflateChildren方法,將剩余的子節點解析并填充到根節點temp
 * rInflateChildren方法后面介紹
 */
 rInflateChildren(parser, temp, attrs, true);
/* 
 * 這時根節點temp已經解析完畢,并已正確添設置LayoutParams,
 * 如果參數root非空,且參數attachToRoot為true,則將temp添加到root上
 */
if (root != null && attachToRoot) {
    root.addView(temp, params);
}

/* 
 * 在開始的時候,result被設置為root,
 * 當root為空,或者attachToRoot為false,即temp沒有被添加到root時
 * 應該將temp作為結果返回,即把result設置為temp
 */
if (root == null || !attachToRoot) {
    result = temp;
}

/*  清空context引用 */
mConstructorArgs[0] = lastContext;
mConstructorArgs[1] = null;

/* 返回結果 */
return result;
inflate過程調用的幾個重要方法:

rInflate方法:
例行先源碼,刪減部分

void rInflate(XmlPullParser parser, View parent, Context context,
                AttributeSet attrs, boolean finishInflate) 
                throws XmlPullParserException, IOException {

    final int depth = parser.getDepth();
    int type;

    while (((type = parser.next()) != XmlPullParser.END_TAG ||
        parser.getDepth() > depth) && type != XmlPullParser.END_DOCUMENT) {

        if (type != XmlPullParser.START_TAG) {
            continue;
        }

        final String name = parser.getName();
        if (TAG_REQUEST_FOCUS.equals(name)) {
            parseRequestFocus(parser, parent);
        } else if (TAG_TAG.equals(name)) {
            parseViewTag(parser, parent, attrs);
        } else if (TAG_INCLUDE.equals(name)) {
            if (parser.getDepth() == 0) {
                throw new InflateException("<include /> cannot be the root element");
            }
            parseInclude(parser, context, parent, attrs);
        } else if (TAG_MERGE.equals(name)) {
            throw new InflateException("<merge /> must be the root element");
        } else {
            final View view = createViewFromTag(parent, name, context, attrs);
            final ViewGroup viewGroup = (ViewGroup) parent;
            final ViewGroup.LayoutParams params = viewGroup.generateLayoutParams(attrs);
            rInflateChildren(parser, view, attrs, true);
            viewGroup.addView(view, params);
        }
    }

    if (finishInflate) {
        parent.onFinishInflate();
    }
}

進一步分析:

/*
 * 這里獲取了xml的當前層,用于后面的層次校驗,
 * 保證while循環于當前節點內,例如:
 * 當前層為1,當循環到層1時,表示當前節點的子節點全部解析完成
 */
final int depth = parser.getDepth();
int type;

/*
 * 循環條件為:
 * 層次深于當前層的節點的結尾(即不為當前節點的結尾)并且不為文件結尾
 * 這樣確保了解析所有子節點,直到文件結尾,或子節點解析完成
 */
while (((type = parser.next()) != XmlPullParser.END_TAG || parser.getDepth() > depth)
       && type != XmlPullParser.END_DOCUMENT) {

    /*
     * 確保從節點開始解析節點
     */
    if (type != XmlPullParser.START_TAG) {
        continue;
    }

    /* 這里的流程下面介紹 */
    ...
}
/* 
 * 根據不同的標簽進行不同的處理,主要針對幾個特殊標簽:
 * <requestFocus/>:使用在非容器View內,能使View獲取焦點
 * <tag/>         :能在xml進行Tag設置,相當于setTag(id, value)
 * <include/>     :include只能作為子節點使用
 * <merge/>       :merge只能作為根節點使用
 * 具體的parse相對簡單,可以自行了解
 * 而不是這幾個特殊標簽時,進入正常解析流程
 */
final String name = parser.getName();
if (TAG_REQUEST_FOCUS.equals(name)) {
    parseRequestFocus(parser, parent);
} else if (TAG_TAG.equals(name)) {
    parseViewTag(parser, parent, attrs);
} else if (TAG_INCLUDE.equals(name)) {
    if (parser.getDepth() == 0) {
        throw new InflateException("<include /> cannot be the root element");
    }
    parseInclude(parser, context, parent, attrs);
} else if (TAG_MERGE.equals(name)) {
    throw new InflateException("<merge /> must be the root element");
} else {

    /*
     * 正常解析流程,看得出這個流程使用了遞歸算法
     * 調用createViewFromTag,生成當前標簽對應的View對象,
     * 然后通過其父容器得到LayoutParams,
     * 然后調用rInflateChildren對該節點下的子節點進行解析填充,
     * 最后,將這個View添加到父容器中。
     */
    final View view = createViewFromTag(parent, name, context, attrs);
    final ViewGroup viewGroup = (ViewGroup) parent;
    final ViewGroup.LayoutParams params = viewGroup.generateLayoutParams(attrs);
    rInflateChildren(parser, view, attrs, true);
    viewGroup.addView(view, params);
}

rInflateChildren方法:

/*
 * 不做過多解析,看得出,rInflateChildren只是對rInflate的簡單封裝
 */
final void rInflateChildren(XmlPullParser parser, View parent, 
                    AttributeSet attrs,boolean finishInflate)
                    throws XmlPullParserException, IOException {

    rInflate(parser, parent, parent.getContext(), attrs, finishInflate);
}

createViewFromTag方法:

View createViewFromTag(View parent, String name, Context context,
                       AttributeSet attrs, boolean ignoreThemeAttr) {

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

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

    if (name.equals(TAG_1995)) {
        return new BlinkLayout(context, attrs);
    }

    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;
    } catch (Exception e) {
        ...
    }
}

這里就涉及到分析一提及到的Factory了,繼續慢慢分析:

/*
 * 對于<view/>標簽,取出其class屬性替代標簽名稱
 */
if (name.equals("view")) {
    name = attrs.getAttributeValue(null, "class");
}

// 這里會產生一個問題,什么是view標簽,class屬性又是什么
// 其實view標簽是為內部類View使用的,例如:
//   在com.example.A.class里,有一個自定義View:ViewB.class,
//   在編譯后,ViewB類名則變成了com.example.A.class$ViewB,
//   而<com.example.A$ViewB/>標簽是不能定義的,所以就要使用到<view/>標簽
//   即<view class="com.example.A$ViewB"/>
// 當然你也可以對普通的View使用同樣的定義方法如<view class="Button"/>
// 注意,使用<view/>標簽必須定義class,否則會拋空指針異常
/*
 * 處理主題相關
 */
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();
}
/*
 * 這個TAG_1995其實就是<blink/>標簽
 * 會為這個標簽創建一個BlinkLayout直接返回
 * 這個BlinkLayout,會對內部的控件提供一種閃爍的功能
 * 然而是沒什么卵用的,其實就是內部維護了一個handler,
 * 每0.5秒進行一次更新,根據mBlinkState去選擇性繪制子控件
 * 這個0.5秒是個final值,不能改變
 */
if (name.equals(TAG_1995)) {
    return new BlinkLayout(context, attrs);
}
/*
 * 這里就是View的創建位置,調用優先級為:
 * 自定義Factory > 系統Factory > LayoutInflater默認
 */
try {
    View view;

    if (mFactory2 != null) {

        /*
         * 優先使用Factory2
         */
        view = mFactory2.onCreateView(parent, name, context, attrs);
    } else if (mFactory != null) {
        
        /*
         * 然后再是Factory
         */
        view = mFactory.onCreateView(name, context, attrs);
    } else {
        view = null;
    }

    /* 這里的流程下面介紹 */
    ...
}
/*
 * 沒有設置自定義Factory時,嘗試使用系統添加的Factory進行創建
 */
if (view == null && mPrivateFactory != null) {
    view = mPrivateFactory.onCreateView(parent, name, context, attrs);
}
/*
 * 沒有設置任何工廠時,使用默認的createView方法創建View
 */
if (view == null) {

   /*
    * 這里也是臨時記錄context
    */
   final Object lastContext = mConstructorArgs[0];
    mConstructorArgs[0] = context;

    try {
        
        /*
         * 這里其實就是看你的view有沒有完整的類名
         * 類似Button,那么就是不完整,使用onCreateView
         * 類似com.example.ViewA這樣的,就可以直接使用createView
         */
        if (-1 == name.indexOf('.')) {
            view = onCreateView(parent, name, attrs);
        } else {
            view = createView(name, null, attrs);
        }
    } finally {

        /*
         * 還原臨時記錄
         */
        mConstructorArgs[0] = lastContext;
    }
}

// 返回View
return view;

onCreateView方法:

/*
 * 這個方法就是對系統提供的控件補完類名"android.view."
 * 實際上工作的還是createView方法
 */
protected View onCreateView(String name, AttributeSet attrs)
                throws ClassNotFoundException {
    return createView(name, "android.view.", attrs);
}

createView方法:
還是先貼源碼,在這個createView里面,涉及到我們之前說的Filter了,直接解析流程

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;

    try {
        if (constructor == null) {

            /*
             * onCreateView方法的"android.view."在這里拼接
             * prefix + name
             */
            clazz = mContext.getClassLoader().loadClass(
                    prefix != null ? (prefix + name) : name).asSubclass(View.class);
            
            /*
             * 這里就是Filter作出過濾的地方了
             * 不通過過濾就會通過failNotAllowed排除異常
             */
            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 {

            /*
             * 有構造器緩存的話,直接使用Filter做一次過濾
             * 這里做了個優化,緩存了Filter的過濾結果
             */
            if (mFilter != null) {
                // 查找Filter緩存
                Boolean allowedState = mFilterMap.get(name);
                if (allowedState == null) {
                    clazz = mContext.getClassLoader().loadClass(
                            prefix != null ? (prefix + name) : name).asSubclass(View.class);
                        
                    boolean allowed = clazz != null && mFilter.onLoadClass(clazz);
                    // 插入Filter緩存
                    mFilterMap.put(name, allowed);
                    if (!allowed) {
                        failNotAllowed(name, prefix, attrs);
                    }
                } else if (allowedState.equals(Boolean.FALSE)) {
                    failNotAllowed(name, prefix, attrs);
                }
            }
        }

        /*
         * 到了這里就知道mConstructorArgs對象里面存放的是什么
         * 分別是context和attrs,老鐵,沒毛病
         */
        Object[] args = mConstructorArgs;
        args[1] = attrs;

        /*
         * newInstance了,反射,沒毛病
         */
        final View view = constructor.newInstance(args);

        /*
         * 像是ViewStub這樣的情況
         * 則將自己(LayoutInflater)復制一份給ViewStub
         * 延遲加載
         */
        if (view instanceof ViewStub) {
            final ViewStub viewStub = (ViewStub) view;
            viewStub.setLayoutInflater(cloneInContext((Context) args[0]));
        }
        
        /*
         * 到這里終于結束這個創建流程了
         */
        return view;
    } catch (NoSuchMethodException e) {
        ...
    }
}

上面整個流程涉及到的幾個點需要說一下:

Filter和Factory:
因為Filter的調用是在LayoutInflater的默認創建過程才會調用的,如果攔截的View在Factory中被返回,則Filter是不會起效的,你也可以在Factory中自行調用Filter進行攔截判斷。

創建子節點和本節點:
所有的創建本節點,都是使用createViewFromTag方法,
某一個節點的子節點創建,都是使用了rInflateChildren方法。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 227,572評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,071評論 3 414
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 175,409評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,569評論 1 307
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,360評論 6 404
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 54,895評論 1 321
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 42,979評論 3 440
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,123評論 0 286
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,643評論 1 333
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,559評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,742評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,250評論 5 356
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 43,981評論 3 346
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,363評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,622評論 1 280
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,354評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,707評論 2 370

推薦閱讀更多精彩內容