1. 程式人生 > >LayoutInflater 載入佈局檔案原理,過程分析

LayoutInflater 載入佈局檔案原理,過程分析

       工作之餘,研究了研究,寫了一個外掛換膚的小框架,準備這段時間寫兩三篇文章做一下總結,有問題的話歡迎批評指正,因為侵入式外掛換膚的框架中有涉及到LayoutInflater載入佈局檔案的相關知識,所以,本篇文章先針對LayoutInflater載入佈局的過程以及原理做一些分析,網上也有不少的相關的文章,也感謝優秀的Android大牛們的分享,為我們平時的開發以及學習提供了很好的借鑑。

      話不多說,下面開始:

      文章的重點會放在LayoutInflater載入佈局檔案的原理以及過程上,這裡稍微提一下LayoutInflater的獲取方式:

方式一:通過Context的getSystemService(String name)方法,熟悉Context的朋友應該知道,Context是一個抽象類,它有兩個實現類ContextWrapper和ContextImpl,我們來看下ContextWrapper的getSystemService()方法

@Override
    public Object getSystemService(String name) {
        return mBase.getSystemService(name);
    }

我們可以看到,該方法內部呼叫了mBase.getSystemService()的方法,這個mBase是ContextImpl類的物件,在這裡我就不解釋為什麼是ContextImpl類的物件了,因為這不是本文的重點,考慮以後有時間寫一篇專門介紹Context的文章,在這裡知道就行了。我們來看下ContextImpl類的getSystemService()方法:

@Override
    public Object getSystemService(String name) {
        return SystemServiceRegistry.getSystemService(this, name);
    }

我用android sdk版本是26,不同的sdk版本ContextImpl中getSystemService()方法的實現可能不太一樣,我們再來看一下SystemServiceRegistery.getSystemService()方法的原始碼

public static Object getSystemService(ContextImpl ctx, String name) {
        ServiceFetcher<?> fetcher = SYSTEM_SERVICE_FETCHERS.get(name);
        return fetcher != null ? fetcher.getService(ctx) : null;
    }

不管使用的sdk版本是多少,但是最終應該都會呼叫到上述程式碼來獲取LayoutInflater物件,這裡簡單說一下,SYSTEM_SERVICE_FETCHERS是一個HasMap物件,儲存了很多的服務 ,準確的說,是儲存了很多服務的獲取器,就是ServiceFetcher,通過獲取器的getService方法,最終獲取到對應的服務,除了LayoutInflater之外,還有WindowManager、ActivityManager等等。

方法二:通過LayoutInflater.from(Context 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(String name)方法來獲取物件

方法三:如果是在Activity中,可以通過Activity的getLayoutInflater方法獲取,我們來看下該方法的程式碼

public LayoutInflater getLayoutInflater() {
        return getWindow().getLayoutInflater();
    }

getWindow()獲得一個Window的物件,Window的實現類時PhoneWindow,我們來看下PnhoneWindow的getLayoutInflater()方法

@Override
    public LayoutInflater getLayoutInflater() {
        return mLayoutInflater;
    }

該方法直接return了一個LayoutInflater的物件,該物件是在PhoneWindow的構造方法裡建立的

public PhoneWindow(Context context) {
        super(context);
        mLayoutInflater = LayoutInflater.from(context);
    }

可以看到在PhoneWindow的構造方法裡面,是通過LayoutInflater.from(Context)來獲得的一個LayoutInflater物件

因此上述三種方法最終都是通過Context.getSystemService()來獲得的LayoutInflater物件。

下面我們來看一下LayoutInflater是怎麼把xml檔案轉換成View的:

入口是LayoutInflater的inflate()方法,inflater有四個不同的實現方法

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

下面我們以public View inflate(@LayoutRes int resource, @Nullable ViewGroup root) 為例,來分析一下LayoutInflater把xml檔案轉換成View的原理和過程

public View inflate(@LayoutRes int resource, @Nullable ViewGroup root) {
        return inflate(resource, root, root != null);
    }

我們可以看到在該方法的內部又呼叫了另一個三個引數的inflate方法

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);
        try {
            return inflate(parser, root, attachToRoot);
        } finally {
            parser.close();
        }
    }

在該方法裡通過Resources的getLayout(Resource)方法獲得一個XmlResourceParser物件,用該物件作為引數呼叫另一個inflate()方法,在這裡我稍微解釋一下XmlResourceParser和Resources的getLayout()方法:

public interface XmlResourceParser extends XmlPullParser, AttributeSet, AutoCloseable {
    /**
     * Close this interface to the resource.  Calls on the interface are no
     * longer value after this call.
     */
    public void close();
}

 XmlResourceParser可以理解成Xml檔案的直譯器,是一個介面,定義了一系列的解析xml檔案的API

 接下來我們來看下Resources的getLayout()方法是怎麼獲取到XmlResourceParser物件的

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

getLayout()方法呼叫了loadXmlResourceParser()方法

1     @NonNull
2     XmlResourceParser loadXmlResourceParser(@AnyRes int id, @NonNull String type)
3             throws NotFoundException {
4         final TypedValue value = obtainTempTypedValue();
5         try {
6             final ResourcesImpl impl = mResourcesImpl;
7             impl.getValue(id, value, true);
8             if (value.type == TypedValue.TYPE_STRING) {
9                 return impl.loadXmlResourceParser(value.string.toString(), id,
10                        value.assetCookie, type);
11            }
12            throw new NotFoundException("Resource ID #0x" + Integer.toHexString(id)
13                    + " type #0x" + Integer.toHexString(value.type) + " is not valid");
14        } finally {
15            releaseTempTypedValue(value);
16        }
17    }

由於loadXmlResourceParser方法的程式碼量略大,加上了行號方便解釋,在該方法的第6行中,獲得了一個ResourcesImpl物件impl,在第9行通過呼叫impl.loadXmlResourceParser()來獲取一個XmlResourceParser

1   @NonNull
2   XmlResourceParser loadXmlResourceParser(@NonNull String file, @AnyRes int id, int 
3       assetCookie,@NonNull String type) throws NotFoundException {
4        if (id != 0) {
5            try {
6                synchronized (mCachedXmlBlocks) { 
7                    final int[] cachedXmlBlockCookies = mCachedXmlBlockCookies;
8                    final String[] cachedXmlBlockFiles = mCachedXmlBlockFiles;
9                    final XmlBlock[] cachedXmlBlocks = mCachedXmlBlocks;
10                    // First see if this block is in our cache.
11                    final int num = cachedXmlBlockFiles.length;
12                    for (int i = 0; i < num; i++) {
                          /*
                           *如果快取中存在符合條件的XmlBlock,就通過呼叫快取的XmlBlock物件的
                           * newParser()方法來獲取XmlResourceParser物件
                           */ 
13                        if (cachedXmlBlockCookies[i] == assetCookie && 
14     cachedXmlBlockFiles[i] != null
15                                && cachedXmlBlockFiles[i].equals(file)) {
16                            return cachedXmlBlocks[i].newParser();
17                        }
18                    }
19
20                    // Not in the cache, create a new block and put it at
21                   // the next slot in the cache.
                     //通過AssetManager物件的openXmlBlockAsset方法獲得一個XmlBlock物件
22                   final XmlBlock block = mAssets.openXmlBlockAsset(assetCookie, file);
23                    if (block != null) {
24                        final int pos = (mLastCachedXmlBlockIndex + 1) % num;
25                        mLastCachedXmlBlockIndex = pos;
26                        final XmlBlock oldBlock = cachedXmlBlocks[pos];
27                        if (oldBlock != null) {
28                            oldBlock.close();
29                        }
30                        cachedXmlBlockCookies[pos] = assetCookie;
31                        cachedXmlBlockFiles[pos] = file;
32                        cachedXmlBlocks[pos] = block;
                          //呼叫XmlBlock物件的newParser()方法來獲得XmlResourceParser物件
33                        return block.newParser();
34                    }
35                }
36            } catch (Exception e) {
37                final NotFoundException rnf = new NotFoundException("File " + file
38                        + " from xml type " + type + " resource ID #0x" + 
39           Integer.toHexString(id));
40                rnf.initCause(e);
41                throw rnf;
42            }
43        }
44
45        throw new NotFoundException("File " + file + " from xml type " + type + " 
46   resource ID #0x"
47                + Integer.toHexString(id));
48    }

上述程式碼中的中文部分是我加的註釋,基本上可以清楚,在loadXmlResourceParser()方法中,如果快取中有XmlBlock物件就從快取中獲取,否則的話就通過呼叫AssetManager中的openXmlBlockAsset()方法來獲得一個XmlBlock物件,最終都會呼叫XmlBlock物件的newParser()方法來獲得一個XmlResourceParser物件,具體newParser()方法中程式碼邏輯是怎樣的,我就不再往下追溯了,有興趣的朋友可以自己研究一下,這樣的話在inflate()方法中就通過Resources的getLayout方法獲得了一個XmlResourceParser物件,好,下面我們回到上文提到的inflate()方法中來,在獲得XmlResourceParser物件之後,把XmlResourceParser的物件作為引數,呼叫另一個inflate()方法,程式碼如下:

1  public View inflate(XmlPullParser parser, @Nullable ViewGroup root, boolean 
2  attachToRoot) {
3         synchronized (mConstructorArgs) {
4             Trace.traceBegin(Trace.TRACE_TAG_VIEW, "inflate");
 
5             final Context inflaterContext = mContext;
              //XmlResourceParser多繼承了XmlPullParser、AttributeSet等介面
              //把XmlPullParser物件強轉成AttributeSet物件  
6             final AttributeSet attrs = Xml.asAttributeSet(parser);
7             Context lastContext = (Context) mConstructorArgs[0];
8             mConstructorArgs[0] = inflaterContext;
9             View result = root;

10            try {
11                // Look for the root node.
12                int type;
                  //該while迴圈的作用應該是尋找  整個佈局檔案的根節點
13                while ((type = parser.next()) != XmlPullParser.START_TAG &&
14                        type != XmlPullParser.END_DOCUMENT) {
15                    // Empty
16                }

                  //如果沒有找到根節點,就會報錯
17                if (type != XmlPullParser.START_TAG) {
18                    throw new InflateException(parser.getPositionDescription()
19                            + ": No start tag found!");
20                }

                  //獲取根節點的名稱
21                final String name = parser.getName();

22                if (DEBUG) {
23                    System.out.println("**************************");
24                    System.out.println("Creating root view: "
25                            + name);
26                    System.out.println("**************************");
27                }
                
                  //如果根節點是merge標籤 
28                if (TAG_MERGE.equals(name)) {
                     /*如果根節點是merge標籤
                      *但是父View,即root為null,或不新增到root中,即attachToRoot為false    
                      *會報錯  
                      */  
29                    if (root == null || !attachToRoot) {
30                        throw new InflateException("<merge /> can be used only 
31  with a valid "
32                                + "ViewGroup root and attachToRoot=true");
33                    }
                      
                      //遞迴生成子節點                       
34                    rInflate(parser, root, inflaterContext, attrs, false);
35                } else {
36                    // Temp is the root view that was found in the xml
                      // 通過createViewFromTag生成根節點View   
37                    final View temp = createViewFromTag(root, name, 
38  inflaterContext, attrs);

39                    ViewGroup.LayoutParams params = null;

40                    if (root != null) {
41                        if (DEBUG) {
42                            System.out.println("Creating params from root: " +
43                                    root);
44                        }
45                        // Create layout params that match root, if supplied
                          /*
                           * 呼叫父View即root的generateLayoutParams()方法
                           * 生成根節點View的LayoutParams
                           */ 
46                        params = root.generateLayoutParams(attrs);
47                        if (!attachToRoot) {
48                            // Set the layout params for temp if we are not
49                            // attaching. (If we are, we use addView, below)
                              /*
                               * 如果不需要將根節點View新增到父View中,
                               * 則對根節點View自身設定LayoutParams    
                               */  
50                            temp.setLayoutParams(params);
51                        }
52                    }

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

56                    // Inflate all children under temp against its context.
                      //解析根節點下的子節點
57                    rInflateChildren(parser, temp, attrs, true);
58
59                    if (DEBUG) {
60                        System.out.println("-----> done inflating children");
61                    }

62                    // We are supposed to attach all the views we found (int 
                         temp)
63                    // to root. Do that now.
64                    if (root != null && attachToRoot) {
                          /*
                           * 如果父View不為null,並且根節點需要新增到父View中  
                           * 則呼叫root的addView方法新增根節點的View    
                           */   
65                        root.addView(temp, params);
66                    }

67                    // Decide whether to return the root that was passed in or 
                         the
68                    // top view found in xml.
69                    if (root == null || !attachToRoot) {
                          //如果父View=root為null或者attachToRoot為false則返回根節點
70                        result = temp;
71                    }
72                }

73            } catch (XmlPullParserException e) {
74                final InflateException ie = new InflateException(e.getMessage(), 75      e);
76                ie.setStackTrace(EMPTY_STACK_TRACE);
77                throw ie;
78            } catch (Exception e) {
79                final InflateException ie = new 
80 InflateException(parser.getPositionDescription()
81                        + ": " + e.getMessage(), e);
82                ie.setStackTrace(EMPTY_STACK_TRACE);
83                throw ie;
84            } finally {
85                // Don't retain static reference on context.
86                mConstructorArgs[0] = lastContext;
87                mConstructorArgs[1] = null;

88                Trace.traceEnd(Trace.TRACE_TAG_VIEW);
89            }

90            return result;
91        }
92    }

      關於通過inflate()方法載入View的整個過程,上述程式碼中我已經在比較重要的地方加了中文註釋,稍微再一點,在上述程式碼的第46行,當root不為null時,會呼叫root的generateLayoutParams()方法生成一個LayoutParams物件,generateLayoutParams()是ViewGroup中定義的方法,不同的子類有不同的實現方式,當root != null && attachToRoot == true的時候,會在上述程式碼的第64行呼叫addView()方法,把節點的View新增到父View中。

     下面我們來考慮一種場景,假如root代表的父View是FrameLayout,在需要載入的xml檔案中定義了android:layout_centerHorizontal屬性,該屬性將會失效,原因就是FrameLayout.LayoutParam中並不支援layout_centerHorizontal屬性,同理當父View是其他的LinearLayout、RelativeLayout等View的時候,如果xml檔案中有不支援的屬性,也會出現失效的問題。

    當傳入的root即父View為null時,xml檔案中以“layout_”開頭的屬性都會消失,因為沒有父View root生成其對應的LayoutParams,如果出入的root為空,根節點定義的寬和高將會失效,因為當一個根節點沒有設定LayoutParams的時候,預設的LayoutParams將會是wrap_content。

下面我們來看下在上述inflate()方法中比較重要的幾個方法rInflate(),rInflateChildren(),以及createViewFromTag()

final void rInflateChildren(XmlPullParser parser, View parent, AttributeSet attrs,
            boolean finishInflate) throws XmlPullParserException, IOException {
        rInflate(parser, parent, parent.getContext(), attrs, finishInflate);
}

我們可以看到在rInflateChidlren()方法內部,也是呼叫了rInflate()方法,下面我們首先來看下載入子節點View的rInflate()方法

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

           //獲取View樹的深度 
4          final int depth = parser.getDepth();
5          int type;

           //當前節點不是尾節點的時候,才會去執行while迴圈內部的邏輯
6          while (((type = parser.next()) != XmlPullParser.END_TAG ||
7                parser.getDepth() > depth) && type != XmlPullParser.END_DOCUMENT) 
8  {

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

              //獲取節點的名稱
12            final String name = parser.getName();
            
13            if (TAG_REQUEST_FOCUS.equals(name)) {
14                parseRequestFocus(parser, parent);
15            } else if (TAG_TAG.equals(name)) {
16                parseViewTag(parser, parent, attrs);
17            } else if (TAG_INCLUDE.equals(name)) {
                  //當節點的標籤是inlcude時候,執行以下操作  
18                if (parser.getDepth() == 0) {
19                    throw new InflateException("<include /> cannot be the root 
20  element");
21                }
                  //解析include的xml檔案
22                parseInclude(parser, context, parent, attrs);
23            } else if (TAG_MERGE.equals(name)) {
24                throw new InflateException("<merge /> must be the root 
25  element");
26            } else {
                   //呼叫createViewFromTag()建立節點View 
27                 final View view = createViewFromTag(parent, name, context, 
28  attrs);
29                 final ViewGroup viewGroup = (ViewGroup) parent;
                  //呼叫generateLayoutParams()建立節點View的LayoutParams  
30                final ViewGroup.LayoutParams params = 
31  viewGroup.generateLayoutParams(attrs);
32                 rInflateChildren(parser, view, attrs, true);
33                 viewGroup.addView(view, params);
34             }
35         }

36         if (finishInflate) {
37             parent.onFinishInflate();
38         }
39 }

我們可以看到,在rInflate()方法中,最終也是會呼叫createViewFromTag()建立節點的View,然後再呼叫rInflateChildren()去遍歷子節點View,最終呼叫父View的addView()方法新增到父View中,到現在為止我們基本能瞭解rInflate()是怎麼遍歷子節點並且建立的View的了,下面我們來看下createViewFromTag()方法是怎麼建立View的,在對createViewFrom()方法分析的過程中,某些地方也是外掛外掛換膚所要掌握的,下面我們來看下createViewFromTag()方法:

1  View createViewFromTag(View parent, String name, Context context, AttributeSet 
2   attrs,boolean ignoreThemeAttr) {
3         //省略部分不重要的程式碼,重點說一下後面View建立的過程
4         ......
5         ......
6         ......

7         try {
8             View view;
              /*
               * Factory2和Factory是兩個介面,
               * 如果我們自己在程式碼中設定了mFactory2或者mFactory, 
               * 在createViewFromTag()方法中建立View的時候就會呼叫mFactory2或者mFactory                                                                                    
               * 的onCrateView()方法,通過我們自己實現的onCreateView()來建立View
               * 侵入式的外掛換膚框架,雖然邏輯有所不同,原理上基本都是自己設定了mFactory2,
               * 實現了onCreateView,用來hook View的建立過程,標記需要被換膚的View  
               */ 
9             if (mFactory2 != null) {
10                view = mFactory2.onCreateView(parent, name, context, attrs);
11            } else if (mFactory != null) {
12                view = mFactory.onCreateView(name, context, attrs);
13            } else {
14                view = null;
15            }

16            if (view == null && mPrivateFactory != null) {
17                view = mPrivateFactory.onCreateView(parent, name, context, 
18                                                     attrs);
19            }
                
              /*
               * 如果沒有設定mFactory2和mFactory,或者最後return null的話,  
               * 就會執行以下程式碼,最終通過反射的方式構建View 
               */ 
20            if (view == null) {
21                final Object lastContext = mConstructorArgs[0];
22                mConstructorArgs[0] = context;
23                try {
24                    if (-1 == name.indexOf('.')) {
                          //onCreateView()最終也是呼叫了27行的createView()方法  
25                        view = onCreateView(parent, name, attrs);
26                    } else {
                          //通過createView()方法來建立View 
27                        view = createView(name, null, attrs);
28                    }
29                } finally {
30                    mConstructorArgs[0] = lastContext;
31                }
32            }

33            return view;
34        } catch (InflateException e) {
35            throw e;

36        } catch (ClassNotFoundException e) {
37            final InflateException ie = new 
38  InflateException(attrs.getPositionDescription()
39                    + ": Error inflating class " + name, e);
40            ie.setStackTrace(EMPTY_STACK_TRACE);
41            throw ie;

42        } catch (Exception e) {
43            final InflateException ie = new 
44   InflateException(attrs.getPositionDescription()
45                    + ": Error inflating class " + name, e);
46            ie.setStackTrace(EMPTY_STACK_TRACE);
47            throw ie;
48        }
49    }

上面程式碼的中文描述還算比較清楚了,在此我再多提一下,在侵入式外掛換膚在標記需要換膚的View的時候,都是通過setFactory2()方法設定mFactory2,重寫onCreateView()方法,hook住View的建立過程,從而標記需要換膚的View。在我後面介紹侵入式外掛換膚的文章裡,也會提到這塊。下面我們來看下上述程式碼27行中的createView()方法

public final View createView(String name, String prefix, AttributeSet attrs)
            throws ClassNotFoundException, InflateException {
        //從快取中通過name獲取View的構造方法
        Constructor<? extends View> constructor = sConstructorMap.get(name);
        
        ......
        
        Class<? extends View> clazz = null;

        try {
            ......

            if (constructor == null) {
                // Class not found in the cache, see if it's real, and try to add it
                /*
                 * 當獲取的構造方法為null時候,表示還未快取過該View的構造方法
                 * 通過ClassLoader的loadClass()載入View對應的class  
                 */
                clazz = mContext.getClassLoader().loadClass(
                        prefix != null ? (prefix + name) : name).asSubclass(View.class);

                ......

                //反射獲取View的構造方法,儲存在Map裡
                constructor = clazz.getConstructor(mConstructorSignature);
                constructor.setAccessible(true);
                sConstructorMap.put(name, constructor);
            } else {
                ......
            }

            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;

            /*
             * mConstructorArgs是一個長度為2的陣列,
             * 把 mConstructorArgs賦值給args,作為引數呼叫constructor構造方法,
             * 從而建立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;

        } catch (NoSuchMethodException e) {
            ......

        } catch (ClassCastException e) {
            ......
        } catch (ClassNotFoundException e) {
            ......
        } catch (Exception e) {
            ......
        } finally {
            ......
        }
    }

通過上述的程式碼可以看出,最終建立View的時候是呼叫的View的兩個引數的構造方法,這也解釋了為什麼我們在自定義View的時候,如果沒有重寫兩個引數的構造方法的話,就會報錯。

好了,基本上LayoutInflater載入xml檔案的整個過程就分析完畢了。!

本人從事工作剛過一年,有些地方可能認識的也沒那麼深刻,大家如果發現什麼問題歡迎批評指正,共同進步,多謝!!!