Android原始碼分析(LayoutInflater.from(this).inflate(resId,null);原始碼解析)

豌豆射手_BiuBiu發表於2018-08-20

原始碼基於安卓8.0分析結果

  • 在這篇文章Android原始碼分析(Activity.setContentView原始碼解析),分析得出,底層走的就是LayoutInflater.from(this),inflate(),如果inflate()傳入的view的話,就呼叫兩次LayoutInflater.from(this),inflate(),,如果是inflate()傳入的resId(佈局的id)的話,就呼叫三次LayoutInflater.from(this),inflate(),,具體請看上篇文章。
  • 怎麼使用,一共有三種的方式,主要看下這種方式LayoutInflater inflater1 = getLayoutInflater();,其實呼叫的就是Activity的getLayoutInflater()方法。
  LayoutInflater inflater1 = getLayoutInflater();//呼叫Activity的getLayoutInflater()
 LayoutInflater inflater2 = LayoutInflater.from(this);
 LayoutInflater inflater3 = (LayoutInflater)this.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
複製程式碼
  • Activity的getLayoutInflater()方法。
  /**
     * Convenience for calling
     * {@link android.view.Window#getLayoutInflater}.
     */
    @NonNull
    public LayoutInflater getLayoutInflater() {
        return getWindow().getLayoutInflater();
    }
複製程式碼
 public PhoneWindow(Context context) {
     super(context);
      mLayoutInflater = LayoutInflater.from(context);
   }
複製程式碼

LayoutInflater inflater2 = LayoutInflater.from(this);的實現的方式

 /**
     * Obtains the LayoutInflater from the given context.
     */
    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;
    }
複製程式碼
  • 以上三種方法,最終還是呼叫了context.getSystemService(Context.LAYOUT_INFLATER_SERVICE)獲取LayoutInflater例項物件。
  • inflate的方法
   public View inflate(XmlPullParser parser, @Nullable ViewGroup root) {
        return inflate(parser, root, root != null);
    }
複製程式碼
  • 到這裡來inflate(@LayoutRes int resource, @Nullable ViewGroup root, boolean attachToRoot),個人的對這個方法的理解是:把佈局檔案填充成View,如果傳入的root不為null,並且attachToRoottrue,就把view填充到root上,相反就不新增上來!
   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) + ")");
        }
        //這裡通過底層的方法,得到一個XmlResourceParser物件
        final XmlResourceParser parser = res.getLayout(resource);
        try {
            return inflate(parser, root, attachToRoot);
        } finally {
            parser.close();
        }
    }
複製程式碼
  • inflate(parser, root, attachToRoot),特別關心這個方法
 public View inflate(XmlPullParser parser, @Nullable ViewGroup root, boolean attachToRoot) {
        synchronized (mConstructorArgs) {
            //底層的方法,不知道原理
            Trace.traceBegin(Trace.TRACE_TAG_VIEW, "inflate");
            // TODO: 2018/6/5  我沒有搞明白 為啥整個值在這裡做什麼的, 在這個方法裡面都沒有使用
            // from傳入的Context
            final Context inflaterContext = mContext;
            // 判斷parser是否是AttributeSet,如果不是則用XmlPullAttributes去包裝一下。
            final AttributeSet attrs = Xml.asAttributeSet(parser);
            // 儲存之前的Context
            Context lastContext = (Context) mConstructorArgs[0];
            // 賦值為傳入的Context
            mConstructorArgs[0] = inflaterContext;
            // 預設返回的是傳入的Parent
            View result = root;
            try {
                // Look for the root node.
                int type;// 迭代xml中的所有元素,挨個解析
                while ((type = parser.next()) != XmlPullParser.START_TAG &&
                        type != XmlPullParser.END_DOCUMENT) {
                    // Empty
                }
               // //如果沒找到有效的開始標籤則丟擲InflateException
                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)) {
                    // 根節點為空或者不新增到根節點上,則丟擲異常。
                    // 因為“merge”標籤必須是要被新增到父節點上的,不能獨立存在。
                    if (root == null || !attachToRoot) {
                        throw new InflateException("<merge /> can be used only with a valid "
                                + "ViewGroup root and attachToRoot=true");
                    }
                    // 遞迴例項化root(也就是傳入Parent)下所有的View
                    // 如果xml中的節點是merge節點,則呼叫rInflate()--方法  // 遞迴例項化根節點的子View
                    rInflate(parser, root, inflaterContext, attrs, false);
                } else {
                    // Temp is the root view that was found in the xml
                    //通過View的父View,View的名稱、attrs屬性例項化View(內部呼叫onCreateView()和createView())。
                    // TODO: 2018/6/6  // 例項化根節點的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 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);
                        }
                    }

                    if (DEBUG) {
                        System.out.println("-----> start inflating children");
                    }

                    // Inflate all children under temp against its context.
                    // 遞迴例項化跟節點的子View
                    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); // TODO: 2018/6/6      返回父View
                    }
               
                    // Decide whether to return the root that was passed in or the
                    // top view found in xml.
                    // TODO: 2018/6/6  父View是空或者不把填充的View新增到父View)
                    if (root == null || !attachToRoot) {
                        result = temp;  // TODO: 2018/6/6 返回根節點View 
                    }
                }

            } 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.
                //不要在上下文中保留靜態引用。
                // 把這之前儲存的Context從新放回全域性變數中。
                mConstructorArgs[0] = lastContext;
                mConstructorArgs[1] = null;

                Trace.traceEnd(Trace.TRACE_TAG_VIEW);
            }

            return result;
        }
    }
複製程式碼
  • 注意其中有個判斷TAG_MERGE.equals(name)) ,這裡就是merge標籤的開始,以下的問題,都帶著這個疑問往下面看。
   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");
                    }
                    // 遞迴例項化root(也就是傳入Parent)下所有的View
                    // 如果xml中的節點是merge節點,則呼叫rInflate()--方法  // 遞迴例項化根節點的子View
                    rInflate(parser, root, inflaterContext, attrs, false);
  }
複製程式碼
  • 解析到merge標籤的話,就會進入到這個方法rInflate(parser, root, inflaterContext, attrs, false);:遞迴例項化根節點的子View,在這裡說明一點的是這個方法傳入的root會不會為null.這篇文章說過Android原始碼分析(Activity.setContentView原始碼解析),肯定不會為null,一般是我們自己去使用的時候,有可能為null,具體的差別可以參考這篇文章Android LayoutInflater深度解析 給你帶來全新的認識

  • 關於rInflate(parser, root, inflaterContext, attrs, false);方法:遞迴方法用於降序XML層次結構並例項化檢視,例項化它們的子節點,然後呼叫onFinishInflate(),, 完成填充

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

        final int depth = parser.getDepth();
        int type;
        boolean pendingRequestFocus = false;
        // 迭代xml中的所有元素,挨個解析
        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();
            //1、解析請求焦點 ,這個控制元件一直有焦點 requestFocus
            if (TAG_REQUEST_FOCUS.equals(name)) {
                pendingRequestFocus = true;
                consumeChildElements(parser);
            } else if (TAG_TAG.equals(name)) {
                //在包含的檢視上設定鍵標記。
                parseViewTag(parser, parent, attrs);
            } else if (TAG_INCLUDE.equals(name)) {// 如果xml中的節點是include節點,則呼叫parseInclude方法
                if (parser.getDepth() == 0) {
                    throw new InflateException("<include /> cannot be the root element");
                }
                //解析include標籤
                parseInclude(parser, context, parent, attrs);
            } else if (TAG_MERGE.equals(name)) {
                // TODO: 2018/5/23 merge  作為佈局裡面的元素 會報錯的哦 ,注意哦
                //想象下 兩個merge 標籤重合在一起
                throw new InflateException("<merge /> must be the root element");
            } else {
                // TODO: 2018/5/23 其實就是如果是merge標籤,
                // TODO: 2018/5/23  那麼直接將其中的子元素新增到merge標籤parent中,這樣就保證了不會引入額外的層級。
                // 1、我們的例子會進入這裡  // 通過View的名稱例項化View
                final View view = createViewFromTag(parent, name, context, attrs);
                // 2、獲取merge標籤的parent
                final ViewGroup viewGroup = (ViewGroup) parent;
                // 3、獲取佈局引數
                final ViewGroup.LayoutParams params = viewGroup.generateLayoutParams(attrs);
                // 4、遞迴解析每個子元素
                rInflateChildren(parser, view, attrs, true);
                // 5、將子元素直接新增到merge標籤的parent view中
                viewGroup.addView(view, params);
            }
        }

        if (pendingRequestFocus) {
            parent.restoreDefaultFocus();
        }
        /**
         * onFinishInflate() View 中的一個空實現,標記完全填充完成了
         * The method onFinishInflate() will be called after all children have been added.
         * 以後是所有的孩子呼叫完成了,之後就呼叫這個方法
         */
        if (finishInflate) {
            parent.onFinishInflate();
        }
    }

複製程式碼
  • 通過對上面的方法分析,還打了斷點,打斷點注意一點,用模擬器,基本上手機廠商都改了frameWork層的程式碼,打出來是亂的。程式碼會走到這裡來,看程式碼的分析,就把新的View,通過ViewGroup.addView()進去了,這也是merge便籤的原理:merge標籤可以自動消除當一個佈局插入到另一個佈局時產生的多餘的View Group,也可用於替換FrameLayout
               // TODO: 2018/5/23 其實就是如果是merge標籤,
                // TODO: 2018/5/23  那麼直接將其中的子元素新增到merge標籤parent中,這樣就保證了不會引入額外的層級。
                // 1、我們的例子會進入這裡  // 通過View的名稱例項化View
                final View view = createViewFromTag(parent, name, context, attrs);
                // 2、獲取merge標籤的parent
                final ViewGroup viewGroup = (ViewGroup) parent;
                // 3、獲取佈局引數
                final ViewGroup.LayoutParams params = viewGroup.generateLayoutParams(attrs);
                // 4、遞迴解析每個子元素
                rInflateChildren(parser, view, attrs, true);
                // 5、將子元素直接新增到merge標籤的parent view中
                viewGroup.addView(view, params);
複製程式碼
  • merge的原理我們清楚了,分析另外一條線,沒有merge標籤,就會走到else的方法來。
                // 如果根節點是“merge”標籤
                if (TAG_MERGE.equals(name)) {
                   
                } else {
                    final View temp = createViewFromTag(root, name, inflaterContext, attrs);
                    ViewGroup.LayoutParams params = null;
                    if (root != null) {
                             params = root.generateLayoutParams(attrs);
                        if (!attachToRoot) {
                            temp.setLayoutParams(params);
                        }
                    }
                    // 遞迴例項化跟節點的子View start inflating children
                    rInflateChildren(parser, temp, attrs, true);

                    if (DEBUG) {
                        System.out.println("-----> done inflating children");
                    }
                    if (root != null && attachToRoot) {
                        root.addView(temp, params); // TODO: 2018/6/6      返回父View
                    }
           
                    // Decide whether to return the root that was passed in or the
                    // top view found in xml.
                    // TODO: 2018/6/6  父View是空或者不把填充的View新增到父View)
                    if (root == null || !attachToRoot) {
                        result = temp;  // TODO: 2018/6/6 返回根節點View 
                    }
複製程式碼
  • 1、這個方法createViewFromTag(View parent, String name, Context context, AttributeSet attrs)
 private View createViewFromTag(View parent, String name, Context context, AttributeSet attrs) {
        return createViewFromTag(parent, name, context, attrs, false);
    }
複製程式碼
  • 最終呼叫的是這個方法createViewFromTag(parent, name, context, attrs, false);這個方法
  • ,什麼時候會走到下面來,name.equals("view")成立的情況,我記得以前我為了在佈局中加了一根線就是這樣寫的,寫快了,寫成小寫的了view,應該寫成大寫View.
 <view
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"/>
複製程式碼

小寫的view:我自己打的斷點發現,如果根標籤是view的,注意是小寫的,view,這個name會為null ,接著下面就會出現空指標的異常---> 我很聰明 很牛逼

 View createViewFromTag(View parent, String name, Context context, AttributeSet attrs,
            boolean ignoreThemeAttr) {
        if (name.equals("view")) {
          //根據我的斷點的話,只要進來的話,這個那麼就會為null
            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();
        }
複製程式碼

name ==null,就會丟擲ava.lang.NullPointerException: Attempt to invoke virtual method 'boolean java.lang.String.equals(java.lang.Object)' on a null object reference,也就是這個原因,所以寫程式碼還是要仔細,仔細!!

在這裡有個很有意思的佈局,開發中很少使用到,而且也不清楚,作者為啥寫,姑且這樣理解Let's party like it's 1995! 寫這個程式碼的哥們,在1995年經歷了一件很開心的事情

        //如果有這個標籤的話,就出現這個佈局,閃動的佈局
        if (name.equals(TAG_1995)) {
            // Let's party like it's 1995! 寫這個程式碼的哥們,在1995年經歷了一件很開心的事情
            return new BlinkLayout(context, attrs);
        }
複製程式碼

BlinkLayout使用的方式,blink 翻譯過來會一閃一閃的

     <blink
            android:layout_width="wrap_content"
            android:layout_height="wrap_content" >
            <TextView
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:background="@android:color/holo_blue_bright"
                android:text="blink單詞也是這個意思.閃爍頻率500毫秒" />
        </blink>

複製程式碼

BlinkLayout程式碼如下, BLINK_DELAY 這個是閃動的時間間隔,如果開發中需要,改動這個時間就行了,感覺這個佈局也沒啥用!

public  class BlinkLayout extends FrameLayout {
    private static final int MESSAGE_BLINK = 0x42;
    private static final int BLINK_DELAY = 500;
    private boolean mBlink;
    private boolean mBlinkState;
    private final Handler mHandler;

    public BlinkLayout(Context context, AttributeSet attrs) {
        super(context, attrs);
        // TODO: 2018/5/22 對handler  原始碼比較熟悉的話,這裡就是Handler另外的一種使用的方法
        mHandler = new Handler(new Handler.Callback() {
            @Override
            public boolean handleMessage(Message msg) {
                if (msg.what == MESSAGE_BLINK) {
                    if (mBlink) {
                        mBlinkState = !mBlinkState;
                        makeBlink();
                    }
                    //invalidate()是用來重新整理View的,必須是在UI執行緒中進行工作。
                    // 比如在修改某個view的顯示時,呼叫invalidate()才能看到重新繪製的介面
                    invalidate();
                    return true;
                }
                return false;
            }
        });
    }

    private void makeBlink() {
        Message message = mHandler.obtainMessage(MESSAGE_BLINK);
        mHandler.sendMessageDelayed(message, BLINK_DELAY);
    }

    @Override
    protected void onAttachedToWindow() {
        super.onAttachedToWindow();

        mBlink = true;
        mBlinkState = true;

        makeBlink();
    }

    @Override
    protected void onDetachedFromWindow() {
        super.onDetachedFromWindow();

        mBlink = false;
        mBlinkState = true;

        mHandler.removeMessages(MESSAGE_BLINK);
    }

    @Override
    protected void dispatchDraw(Canvas canvas) {
        //根據這個狀態是否去dispatchDraw
        if (mBlinkState) {
            super.dispatchDraw(canvas);
        }
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        return super.onTouchEvent(event);
    }

}
複製程式碼

下面是後續的程式碼,通過工廠建立了view。去定位過,在public class Activity extends ContextThemeWrapper implements LayoutInflater.Factory2,很多的地方,如果需要研究的,還需要更深層的原始碼理解,這裡就先mark下,只需要知道view是通過工廠來得到的

        try {
            View view;
            if (mFactory2 != null) {
                //各個工廠先onCreateView()
                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 (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;
        }
    }
複製程式碼
  • 2、關於這個方法rInflateChildren(parser, temp, attrs, true);:遞迴例項化跟節點的子View.其實呼叫的還是這個方法rInflate(parser, root, inflaterContext, attrs, false);,文章的前部分貼出了這部分的程式碼
    final void rInflateChildren(XmlPullParser parser, View parent, AttributeSet attrs,
            boolean finishInflate) throws XmlPullParserException, IOException {
        rInflate(parser, parent, parent.getContext(), attrs, finishInflate);
    }
複製程式碼

主要是為了分析幾個Tag

private static final String TAG_MERGE = "merge";
    private static final String TAG_INCLUDE = "include";
    private static final String TAG_1995 = "blink";
    private static final String TAG_REQUEST_FOCUS = "requestFocus";
    private static final String TAG_TAG = "tag";
    private static final String ATTR_LAYOUT = "layout";

複製程式碼

requestFocus:解析請求焦點 ,這個控制元件一直有焦點 requestFocus,一般作用於 EditText,需要長期獲取焦點的控制元件,但是真正的實現的方法,我們都是程式碼去實現了,很少這樣使用,這裡就說明一下,有這樣的使用的方法!

 if (TAG_REQUEST_FOCUS.equals(name)) {
                pendingRequestFocus = true;
                consumeChildElements(parser);
   } 
複製程式碼
       <EditText
            android:id="@+id/text"
            android:layout_width="match_parent"
            android:layout_height="wrap_content" >
            <!-- 當前控制元件處於焦點狀態 -->
            <requestFocus />
        </EditText>
複製程式碼

tag:在包含的檢視上設定鍵標記,其實就是給view設定上了一個tag標籤

if (TAG_TAG.equals(name)) {
                //在包含的檢視上設定鍵標記。
                parseViewTag(parser, parent, attrs);
    } 
 final int key = ta.getResourceId(R.styleable.ViewTag_id, 0);
   final CharSequence value = ta.getText(R.styleable.ViewTag_value);
   view.setTag(key, value);
複製程式碼

merge:在這裡如果解析到了merge標籤的話,這裡就直接丟擲異常了,必須是根元素!兩種情況下,一種是套用了兩個merge標籤,或者是一個ViewGruop標籤巢狀了一個merge,都會丟擲異常

 if (TAG_MERGE.equals(name)) {
                // TODO: 2018/5/23 merge  作為佈局裡面的元素 會報錯的哦 ,注意哦
                //想象下 兩個merge 標籤重合在一起
                throw new InflateException("<merge /> must be the root element");
  }
複製程式碼

include:標籤,在這裡如果include作為了一個根元素,也會丟擲異常,當然,如果在程式碼中見到別人那麼用的話,建議自己去面壁思過會!

if (TAG_INCLUDE.equals(name)) {// 如果xml中的節點是include節點,則呼叫parseInclude方法
                if (parser.getDepth() == 0) {
                    throw new InflateException("<include /> cannot be the root element");
                }
                //解析include標籤
                parseInclude(parser, context, parent, attrs);
   }
複製程式碼

parseInclude(parser, context, parent, attrs);,這個方法也就是include必須走的方法,搞明白的話,就可以搞明白原理,而且在特殊的情況下,include標籤會和 merge 標籤一起使用。這個方法也可以解釋include標籤的原理!

  • 可以從這裡可以看到,include只能在ViewGroup中使用,在 'View'中使用的話,會報錯
   private void parseInclude(XmlPullParser parser, Context context, View parent,
            AttributeSet attrs) throws XmlPullParserException, IOException {
        int type;
   if (parent instanceof ViewGroup) {
            // Apply a theme wrapper, if requested. This is sort of a weird
            // edge case, since developers think the <include> overwrites
            // values in the AttributeSet of the included View. So, if the
            // included View has a theme attribute, we'll need to ignore it.
            //如果VIEW檢視有一個主題屬性,我們需要忽略它。
            final TypedArray ta = context.obtainStyledAttributes(attrs, ATTRS_THEME);
            final int themeResId = ta.getResourceId(0, 0);
            final boolean hasThemeOverride = themeResId != 0;
            if (hasThemeOverride) {
                context = new ContextThemeWrapper(context, themeResId);
            }
            ta.recycle();

            //如果佈局指向主題屬性,我們必須
            //按摩該值以從中獲取資源識別符號。
            int layout = attrs.getAttributeResourceValue(null, ATTR_LAYOUT, 0);
            if (layout == 0) {
                final String value = attrs.getAttributeValue(null, ATTR_LAYOUT);
                // TODO: 2018/6/6 include標籤中沒有設定layout屬性,會丟擲異常
                if (value == null || value.length() <= 0) {//
                    throw new InflateException("You must specify a layout in the"
                            + " include tag: <include layout=\"@layout/layoutID\" />");
                }

                // Attempt to resolve the "?attr/name" string to an attribute
                // within the default (e.g. application) package.
                layout = context.getResources().getIdentifier(
                        value.substring(1), "attr", context.getPackageName());

            }

            // The layout might be referencing a theme attribute.
            if (mTempValue == null) {
                mTempValue = new TypedValue();
            }
            if (layout != 0 && context.getTheme().resolveAttribute(layout, mTempValue, true)) {
                layout = mTempValue.resourceId;
            }

            if (layout == 0) {
                final String value = attrs.getAttributeValue(null, ATTR_LAYOUT);
                throw new InflateException("You must specify a valid layout "
                        + "reference. The layout ID " + value + " is not valid.");
            } else {
                final XmlResourceParser childParser = context.getResources().getLayout(layout);

                try {// 獲取屬性集,即在include標籤中設定的屬性
                    final AttributeSet childAttrs = Xml.asAttributeSet(childParser);

                    while ((type = childParser.next()) != XmlPullParser.START_TAG &&
                            type != XmlPullParser.END_DOCUMENT) {
                        // Empty.
                    }

                    if (type != XmlPullParser.START_TAG) {
                        throw new InflateException(childParser.getPositionDescription() +
                                ": No start tag found!");
                    }
                   // 1、解析include中的第一個元素
                    final String childName = childParser.getName();
                   // 如果第一個元素是merge標籤,那麼呼叫rInflate函式解析
                    if (TAG_MERGE.equals(childName)) {
                        // The <merge> tag doesn't support android:theme, so
                        // nothing special to do here.
                        rInflate(childParser, parent, context, childAttrs, false);
                    } else {
                        //2、我們例子中的情況會走到這一步,首先根據include的屬性集建立被include進來的xml佈局的根view
                        // 這裡的根view對應為my_title_layout.xml中的RelativeLayout
                        final View view = createViewFromTag(parent, childName,
                                context, childAttrs, hasThemeOverride);
                        final ViewGroup group = (ViewGroup) parent;

                        final TypedArray a = context.obtainStyledAttributes(
                                attrs, R.styleable.Include);
                        //這裡就是設定的<include>佈局設定的id ,如果include設定了這個id ,那麼這個id就不等於 View.No_ID=-1
                        final int id = a.getResourceId(R.styleable.Include_id, View.NO_ID);
                        final int visibility = a.getInt(R.styleable.Include_visibility, -1);
                        a.recycle();

                        // We try to load the layout params set in the <include /> tag.
                        // If the parent can't generate layout params (ex. missing width
                        // or height for the framework ViewGroups, though this is not
                        // necessarily true of all ViewGroups) then we expect it to throw
                        // a runtime exception.
                        // We catch this exception and set localParams accordingly: true
                        // means we successfully loaded layout params from the <include>
                        // tag, false means we need to rely on the included layout params.
                        ViewGroup.LayoutParams params = null;
                        try {
                            // 獲3、取佈局屬性
                            params = group.generateLayoutParams(attrs);
                        } catch (RuntimeException e) {
                            // Ignore, just fail over to child attrs.
                        }
                        // 被inlcude進來的根view設定佈局引數
                        if (params == null) {
                            params = group.generateLayoutParams(childAttrs);
                        }
                        view.setLayoutParams(params);
                         // 4、Inflate all children. 解析所有子控制元件
                        // Inflate all children.
                        rInflateChildren(childParser, view, childAttrs, true);

                        if (id != View.NO_ID) {
                         // 5、將include中設定的id設定給根view,因此實際上my_title_layout.xml中
                          // 的RelativeLayout的id會變成include標籤中的id,include不設定id,那麼也可以通過relative的找到.
                            view.setId(id);
                        }

                        switch (visibility) {
                            case 0:
                                view.setVisibility(View.VISIBLE);
                                break;
                            case 1:
                                view.setVisibility(View.INVISIBLE);
                                break;
                            case 2:
                                view.setVisibility(View.GONE);
                                break;
                        }
                        // 6、將根view新增到父控制元件中
                        group.addView(view);
                    }
                } finally {
                    childParser.close();
                }
            }
        } else {
            throw new InflateException("<include /> can only be used inside of a ViewGroup");
        }
        //預設的可見性使得LayoutInflater_Delegate代表可以呼叫它。
        LayoutInflater.consumeChildElements(parser);
複製程式碼
  • 分段分析上面的那段程式碼, 1、如果VIEW檢視有一個主題屬性,我們需要忽略它。
            //如果VIEW檢視有一個主題屬性,我們需要忽略它。
            final TypedArray ta = context.obtainStyledAttributes(attrs, ATTRS_THEME);
            final int themeResId = ta.getResourceId(0, 0);
            final boolean hasThemeOverride = themeResId != 0;
            if (hasThemeOverride) {
                context = new ContextThemeWrapper(context, themeResId);
            }
            ta.recycle();
複製程式碼

2、如果佈局指向主題屬性,我們必須從該值以從中獲取資源識別符號!

            //如果佈局指向主題屬性,我們必須
            //按摩該值以從中獲取資源識別符號。
            int layout = attrs.getAttributeResourceValue(null, ATTR_LAYOUT, 0);
複製程式碼

3、在這裡出現value == null || value.length() <= 0的話,就會丟擲異常,也就是說include標籤中沒有設定layout屬性,會丟擲異常。

            if (layout == 0) {
                final String value = attrs.getAttributeValue(null, ATTR_LAYOUT);
                // TODO: 2018/6/6 include標籤中沒有設定layout屬性,會丟擲異常
                if (value == null || value.length() <= 0) {//
                    throw new InflateException("You must specify a layout in the"
                            + " include tag: <include layout=\"@layout/layoutID\" />");
                }
                // Attempt to resolve the "?attr/name" string to an attribute
                // within the default (e.g. application) package.
                layout = context.getResources().getIdentifier(
                        value.substring(1), "attr", context.getPackageName());

            }
複製程式碼

3、解析include中的第一個元素,如果這裡第一個childeNamemerge,那麼就是 includemerge 一起使用的情況,呼叫的方法是rInflate(childParser, parent, context, childAttrs, false);和上面一樣,不會生成多餘的層級

                   // 1、解析include中的第一個元素
                    final String childName = childParser.getName();
                   // 如果第一個元素是merge標籤,那麼呼叫rInflate函式解析
                    if (TAG_MERGE.equals(childName)) {
                        // The <merge> tag doesn't support android:theme, so
                        // nothing special to do here.
                        rInflate(childParser, parent, context, childAttrs, false);
                    } 
複製程式碼

4、首先根據include的屬性集建立被include進來的xml佈局的根view,這個view其實就include 進來的根ViewGroup,因為在前面已經把merge的情況過濾掉了,

                     else {
                        //2、我們例子中的情況會走到這一步,首先根據include的屬性集建立被include進來的xml佈局的根view
                        final View view = createViewFromTag(parent, childName,
                                context, childAttrs, hasThemeOverride);
複製程式碼

5、特別關心這個final int id = a.getResourceId(R.styleable.Include_id, View.NO_ID);函式,其實就是給include標籤設是否設定了id,就好像下面的佈局一樣,如果設定了這個android:id="@+id/my_title_ly"

    <include
        android:id="@+id/my_title_ly"
        android:layout_width="match_parent"
        android:layout_height="200dp"
        layout="@layout/my_include_layout" />
複製程式碼

這個是上面得include的佈局my_include_layout

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:id="@+id/my_title_parent_id"
    android:layout_height="wrap_content" >
   ....
</RelativeLayout>
複製程式碼

下面的這段程式碼就會執行,這也就解釋了:安卓include標籤的找不到id的問題,因為你在include設定了這個id屬性,那麼你找這個my_title_parent_id這個id,findViewById(R.id.my_title_parent_id)肯定也會找出來為null!

                        final int id = a.getResourceId(R.styleable.Include_id, View.NO_ID);
                        if (id != View.NO_ID) {
                         // 5、將include中設定的id設定給根view,因此實際上my_title_layout.xml中
                          // 的RelativeLayout的id會變成include標籤中的id,include不設定id,那麼也可以通過relative的找到.
                            view.setId(id);
                        }

}
複製程式碼

為什麼findViewById(R.id.my_title_parent_id)會找出來為null,簡單敘述下就是在Activity中呼叫方法其實是WindowfindViewById,getDecorView().findViewById(id),也就是PhoneWindow裡面的DecorViewfindViewById,也就是ViewfindViewById;可以看這邊文章Android原始碼分析(事件傳遞)涉及到這幾個類了。如下面的方法,這個View 有id,找id的話就會走findViewTraversal這個方法,找的id和setId不一樣的話,就會返回null,這就是找不到 id的原因。這個原因和ViewStub標籤中設定了inflatedId一樣的效果,下篇文章會講到,非常有意思!已經寫好Android原始碼分析(ViewStub原始碼解析)

    public final <T extends View> T findViewById(@IdRes int id) {
       if (id == NO_ID) {
            return null;
      }
       return findViewTraversal(id);
    }
   protected <T extends View> T findViewTraversal(@IdRes int id) {
      if (id == mID) {
           return (T) this;
      }
      return null;
    }
複製程式碼

View中設定id的方法

    public void setId(@IdRes int id) {
      mID = id;
       if (mID == View.NO_ID && mLabelForId != View.NO_ID) {
            mID = generateViewId();
       }
   }
複製程式碼

6、關於final int visibility = a.getInt(R.styleable.Include_visibility, -1);這行程式碼,也就是說我們設定了include標籤'visibility'其實也就是設定layoutvisibility的屬性!

  final int visibility = a.getInt(R.styleable.Include_visibility, -1);
    switch (visibility) {
                            case 0:
                                view.setVisibility(View.VISIBLE);
                                break;
                            case 1:
                                view.setVisibility(View.INVISIBLE);
                                break;
                            case 2:
                                view.setVisibility(View.GONE);
                                break;
                        }
複製程式碼

7、將根view新增到父控制元件中,最後都會走到這裡來,只不過使用了merge是不會產生多餘的層級的,但是使用的時候有場景的要求!

        group.addView(view);
複製程式碼
  • 最後做了一張圖,謝謝

    LayoutInflater.from(this).inflate(resId,null);程式碼解析.png

  • 說明幾點

    • 佈局轉化為View,關鍵方法就是LayoutInflater.from(this),inflate()
    • PhoneWindow這個類很關鍵
    • 如果需要打斷點,建議使用模擬器,手機的廠商基本上改動過framework的程式碼,打出來不準確
    • merge原理就是通過addView(view),從而減少了層級
    • BlinkLayout這個是LayoutInflater的內部類,感覺是谷歌工程師皮了一下
  • include只能在ViewGroup中使用,在 'View'結點中使用的話,會報錯

相關文章