Android 深入理解LayoutInflater工作機制

Android里面有很多場景會用到LayoutInflate這個類,我們通過這個類去解析指定的布局,然后展示在布局里面。api的調用是如此的簡單,我們如果每次都是單純的調用,那就無法得到提升了,所以現在我們來看一下這個流程究竟是怎么一回事。
先來看下用法跟場景:

@Override
    public AllPavilionViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
        //在recyclerview/listview的適配器中 構建每個item的布局
        return new AllPavilionViewHolder(inflater.inflate(R.layout.item_location_pavilion, parent, false));
    }
 public AmountView(Context context, AttributeSet attrs) {
        super(context, attrs);
        //自定義UI中 解析指定布局去依附在自定義view中
        LayoutInflater.from(context).inflate(R.layout.ui_amount, this);
    }
    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        //觀察源碼 其實這個setContentView 內部也是調用Layoutflate.inflate的方式來構建view的
        setContentView(R.layout.activity_reserve);
    }

可能還有一些其他場景會用到這個方法,這些就不一一舉例了,我們直接來通過源碼分析這塊的流程吧。


   /**
     * Obtains the LayoutInflater from the given context.
     * //首先是初始化 可以看出我們常用的LayoutInflater其實是對如下方法的簡單封裝,意味著我們其實有兩種方式來構建出LayoutInflater的實例
     */
    public static LayoutInflater from(Context context) {
        LayoutInflater LayoutInflater =
                (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
        if (LayoutInflater == null) {
            throw new AssertionError("LayoutInflater not found.");
        }
        return LayoutInflater;//為什么變量名字大寫開頭 我也不知道
    }

//拿到我們想要的實例之后,之后就是調用inflate方法去構建view了。

  //四種inflate方法 到最后還是殊途同歸
 public View inflate(@LayoutRes int resource, @Nullable ViewGroup root) {
        //里面調用的還是下面那個方法 只是做了一點判斷而已
        return inflate(resource, root, root != null);
    }
  //說一下參數 
  //resource代表需要解析的xml文件
  //root 表示這個xml文件外圍包裹的父布局,如果不需要 直接傳null便可 
  //attachToRoot 是否需要布局文件依附在root上 如果root為null的話 那肯定是依附不了的
 public View inflate(@LayoutRes int resource, @Nullable ViewGroup root, boolean attachToRoot) {
        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);//得到一個xml解析器
        try {
            return inflate(parser, root, attachToRoot);//這個方法才是真正的開始解析布局文件
        } finally {
            parser.close();//解析器用完 關掉
        }
    }
  //重點來了 
 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!");
                }
                //開始構建 root view
                final String name = parser.getName();
                if (TAG_MERGE.equals(name)) {
                  //根布局為merge版
                    if (root == null || !attachToRoot) {
                    //判斷根布局是不是merge 如果是的話 需要有父布局并且attachToRoot為true 否則拋出異常
                        throw new InflateException("<merge /> can be used only with a valid "
                                + "ViewGroup root and attachToRoot=true");
                    }

                    rInflate(parser, root, inflaterContext, attrs, false);//這里開始循環解析
                } else {
                       //根布局為非merge版
                    // 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) {//如果root 不為null 然后attachToRoot 為false
                    //只有在這種情況下 他的屬性才會被真正設置進去 否則無效 root!=null&&attachToRoot==false (會影響寬高跟margin  ,關于padding 畢竟是作用在view的onDraw方法里面的,還是有效果的)
                        // 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);
                        }
                    }

                    //開始解析布局文件中 子view 循環解析所有子view
                    // 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) {//判斷是返回剛構建的view 還是之前傳進來的root
                        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;
        }
    }

我們先看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) {//默認ignoreThemeAttr 給的是false
            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();//TypedArray用完都是要回收的
        }

        if (name.equals(TAG_1995)) {
            // Let's party like it's 1995!
            return new BlinkLayout(context, attrs);//一種閃爍的布局 
          //想要看效果 可以看http://blog.csdn.net/qq_22644219/article/details/69367150
        }

        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);//主體還是這個方法 通過反射的方式創建了view
                    }
                } 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;
        }
    }

根view創建好了之后,之后會調用 rInflate(parser, root, inflaterContext, attrs, false)一步步的實現每一個子view

 /**
     * Recursive method used to descend down the xml hierarchy and instantiate
     * views, instantiate their children, and then call onFinishInflate().
     * 遞歸的調用方法構建整個xml的布局,沿層次一個個的實例化view
     */
    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) {//循環拿到那個start_tag
            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);//這里做include內容的解析
            } else if (TAG_MERGE.equals(name)) {//意思是 merge 不能作為一個子view 除非他是根布局
                throw new InflateException("<merge /> must be the root element");
            } else {
              //這里做真正的創建view 還是通過反射的形式 另外把一些屬性設置進去 再添加到父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);
            }
        }
        //在這里 所有的view 都被構建成功 
        if (finishInflate) {//當根布局不是merge 那就為true
            parent.onFinishInflate();//就是一個回調吧
        }
    }

整個流程就是這樣子 ,我們再來看一下,我在最初的時候說 setContentView也是通過LayoutInflate方法的:

 @Override
    public void setContentView(@LayoutRes int layoutResID) {//我們在activity里面的調用 就是這個方法
        getDelegate().setContentView(layoutResID);
    }

//這是一個抽象的方法
    public abstract void setContentView(@LayoutRes int resId);

//然后我們看AppCompatDelegateImplV9的實現
 @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();
    }

總結一下LayoutInflate里面的實現,其實也是很簡單的用了android提供的pull解析一步步的解析下來的,里面的每一個節點就構建成一個view了(通過反射),從根布局開始一層層的解析構建,最終形成一個完整的DOM結構,然后把根布局的引用傳出去,這樣inflate方法就成功完成了。

最后說一下三個inflate的三個參數作用,畢竟有時候會疑惑該如何傳參:

  1. 如果root為null,attachToRoot將失去作用,設置任何值都沒有意義。
  2. 如果root不為null,attachToRoot設為true,則會給加載的布局文件的指定一個父布局,即root,此時是root設置的那些寬高跟margin ,是沒有效果的。
  3. 如果root不為null,attachToRoot設為false,則會將布局文件最外層的所有layout屬性進行設置,當該view被添加到父view當中時,這些layout屬性會自動生效。
  4. 在不設置attachToRoot參數的情況下,如果root不為null,attachToRoot參數默認為true。

還有一點,請不要在listview\recyclerview里面構建每個item的時候設置root!=null 并且attachToRoot又給了true ,這樣會直接報錯的,因為

@Override
  public void addView(View child) {
        throw new UnsupportedOperationException("addView(View) is not supported in AdapterView");
  }

參考:

Android LayoutInflater原理分析,帶你一步步深入了解View(一)
Android LayoutInflate深度解析 給你帶來全新的認識

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

推薦閱讀更多精彩內容