探究Android View 繪製流程,Xml 檔案到 View 物件的轉換過程

看我眼神007發表於2018-01-09
基於 Android API 26 Platform 原始碼

寫作背景

Android 開發框架中,使用 Xml 檔案描述 Ui 頁面,通過setContentView(resId) 或者LayoutInflater.inflate(resId,……)的方式把 Xml 檔案描述的頁面轉換成 Java 物件。Xml 檔案加上 AndroidStudio 提供的預覽功能,使得 Android 開發過程中頁面和業務邏輯可以並行開發,極大的提高了開發效率。

但是大部分 Android 工程師對 xml 檔案如何轉換成 Java 不是十分了解,本文將帶大家一起探究 View 從 xml 檔案到 Java 物件的轉換過程

xml 轉成成 Java 物件有幾種方式?

我們先羅列一下 xml 轉換成 Java 物件的方式

1. 在 Activity中呼叫 setContentView(resId)
2. LayoutInflater.from(context).inflate(resId,……) 
複製程式碼

跟蹤一下 Activity.setContentView(resId)

我們一般在專案使用的 Activity 可能是

1. android.support.v7.app.AppCompatActivity
2. android.support.v4.app.FragmentActivity
3. android.app.Activity
4. 其他 Activity
複製程式碼

所以的 Activity 都是 android.app.Activity 的子類。

但是!每個繼承 android.app.Activity 的子類 setContentView(resId) 實現方式都被過載了。我們這裡先看最基礎的 android.app.Activity

public void setContentView(@LayoutRes int layoutResID) {
    getWindow().setContentView(layoutResID);
    initWindowDecorActionBar();
}
複製程式碼

檢視一下 getWindow()原始碼

public Window getWindow() {
    return mWindow;
}
複製程式碼

全域性搜尋 mWindow 物件賦值的地方找到以下程式碼

mWindow = new PhoneWindow(this, window, activityConfigCallback);
複製程式碼

這裡 PhoneWindow 的原始碼在 sdk 裡面是隱藏的,我們去 androidxref ->PhoneWindow.java 檢視 PhoneWindow.setContentView(layoutResID)

@Override
public void setContentView(int layoutResID) {
    // Note: FEATURE_CONTENT_TRANSITIONS may be set in the process of installing the window
    // decor, when theme attributes and the like are crystalized. Do not check the feature
    // before this happens.
    if (mContentParent == null) {
        installDecor();
    } else if (!hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
        mContentParent.removeAllViews();
    }

    if (hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
        final Scene newScene = Scene.getSceneForLayout(mContentParent, layoutResID,
                getContext());
        transitionTo(newScene);
    } else {
        mLayoutInflater.inflate(layoutResID, mContentParent);
    }
    mContentParent.requestApplyInsets();
    final Callback cb = getCallback();
    if (cb != null && !isDestroyed()) {
        cb.onContentChanged();
    }
}
複製程式碼

當我們沒有設定專場動畫的時候會執行

mLayoutInflater.inflate(layoutResID, mContentParent);
複製程式碼

在 PhoneWindow 的建構函式中我們找到了 mLayoutInflater 物件賦值語句

public PhoneWindow(Context context) {
    super(context);
    mLayoutInflater = LayoutInflater.from(context);
}
複製程式碼

所以我們得出一個結論 Activity.setContentView(resId) 最終還是使用LayoutInflater.from(context).inflate(resId, ……)

再回頭看下 android.support.v7.app.AppCompatActivityandroid.support.v4.app.FragmentActivity 我們發現 android.support.v4.app.FragmentActivity 沒有過載 android.app.Activity.setContentView(resId) 但是 android.support.v7.app.AppCompatActivity 過載了

@Override
public void setContentView(@LayoutRes int layoutResID) {
    getDelegate().setContentView(layoutResID);
}
複製程式碼

再跟蹤一下原始碼我們發現最終會呼叫到 android.support.v7.app.AppCompatDelegateImplV9.setContentView(resId)

@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();
}
複製程式碼

這裡我們又發現了 LayoutInflater 的身影。

這裡我們可以總結一下 xml 轉成成 Java 物件是通過 LayoutInflater 的 inflate() 方法來完成的

LayoutInflater 物件如何例項化

看下一下 LayoutInflater 的原始碼第一行

public abstract class LayoutInflater {……}
複製程式碼

LayoutInflater 是一個抽象類, 抽象類是不能例項化的

confuse.jpg

先想一下 LayoutInflater 物件獲取的方式

1. 在 Activity 中通過 getLayoutInflater() 獲取
2. 通過 LayoutInflater.from(context) 獲取
3.context.getSystemService(Context.LAYOUT_INFLATER_SERVICE) 獲取
複製程式碼

看 Activity 的 getLayoutInflater()

public LayoutInflater getLayoutInflater() {
    return getWindow().getLayoutInflater();
}    
複製程式碼

這裡我們就可以看出 Activity 通過 getLayoutInflater() 獲取的是 PhoneWindow 的 mLayoutInflater (如果忘記了可以往上翻一下,或者去參考資料的連結裡找找原始碼)

再看一下 LayoutInflater.from(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;
}
複製程式碼

此時,我們必須請出柯南君幫我們宣佈

kenan_01.jpg

真相只有一個!最終都是通過服務獲取 LayoutInflater 例項物件

下一步,原始碼追蹤context.getSystemService(Context.LAYOUT_INFLATER_SERVICE)
這裡先說明一個前提,context 的實現類是 ContextImpl
複製程式碼

如果對該前提有疑問請移步 Android Context完全解析,你所不知道的Context的各種細節

所以我們直接檢視 ContextImpl.getSystemService(Context.LAYOUT_INFLATER_SERVICE)

@Override
public Object getSystemService(String name) {
    return SystemServiceRegistry.getSystemService(this, name);
}
複製程式碼

繼續跟蹤 SystemServiceRegistry

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

這時候我們在 SystemServiceRegistry 類停留一下,發現這裡似乎只註冊各種系統服務的地方。我們找到了 Context.LAYOUT_INFLATER_SERVICE 註冊程式碼。

static {
     ……
     registerService(Context.LAYOUT_INFLATER_SERVICE, LayoutInflater.class,
             new CachedServiceFetcher<LayoutInflater>() {
         @Override
          public LayoutInflater createService(ContextImpl ctx) {
             return new PhoneLayoutInflater(ctx.getOuterContext());
         }});
     ……        
 }

 private static <T> void registerService(String serviceName, Class<T> serviceClass,
         ServiceFetcher<T> serviceFetcher) {
     SYSTEM_SERVICE_NAMES.put(serviceClass, serviceName);
     SYSTEM_SERVICE_FETCHERS.put(serviceName, serviceFetcher);
 }
複製程式碼

然後我們終於找到 LayoutInflater 的實現類是 PhoneLayoutInflater

此時我們可以休息一下,喝口水,上個衛生間,進入下個階段

LayoutInflater 讀取 xml 檔案並建立 View 物件

LayoutInflater.inflate()

public View inflate(@LayoutRes int resource, @Nullable ViewGroup root)
public View inflate(@LayoutRes int resource, @Nullable ViewGroup root, boolean attachToRoot)
複製程式碼

再去原始碼檢視一下,發現兩個方法其實只有一個方法是核心,另一個只是做了一下封裝,讓我們少傳入一個引數。

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

所以我們重點看一下 inflate(@LayoutRes int resource, @Nullable ViewGroup root, boolean attachToRoot) 的原始碼

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();
    }
}
複製程式碼

我們看到首先通過 res 物件把 resId 指向的 xml 檔案轉換為 XmlResourceParser 然後執行 inflate(parser, root, attachToRoot) 方法,該方法比較長,這裡只貼出核心步驟。

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 {
            ……

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

        } …… 
          省略異常處理部分
          ……

        return result;
    }
}
複製程式碼

以上步驟還是很長,我們將拆分幾部分分析。

第一部分
    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);
    }
複製程式碼

如果 xml 根標籤是 mergin,則 root 不能為空, attachToRoot 必須是 true。

然後執行 rInflate(parser, root, inflaterContext, attrs, false)

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;

    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)) {
            pendingRequestFocus = true;
            consumeChildElements(parser);
        } 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 (pendingRequestFocus) {
        parent.restoreDefaultFocus();
    }

    if (finishInflate) {
        parent.onFinishInflate();
    }
}
複製程式碼

上面這個方式我們需要重點記一下

1. 遍歷該節點的子節點
2. 子節點有 "requestFocus"、"tag"、""、"include" 
3. 子節點不能是 "merge"
4. 子節點的其他情況,則是各種 View 的標籤
5. View 標籤和  "include" 標籤會建立 View 物件
6. 遍歷結束以後執行 parent.onFinishInflate()
複製程式碼

如果子節點是 include 則執行 parseInclude() ,parseInclude() 的原始碼和 inflate(parser, root, attachToRoot) 類似,都是***讀取xml對應的檔案,轉換成 XmlResourceParser 然後遍歷裡的標籤***

經過層層呼叫,我們可以找到最終建立 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);
複製程式碼

第一部分程式碼,我們的到的結論是, createViewFromTag(parent, name, context, attrs)負責建立 View 物件

第二部分
  // 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;
   }
複製程式碼

因為這裡排除了mergin標籤,這裡的根標籤肯定是一個 View,所以呼叫了 createViewFromTag(root, name, inflaterContext, attrs) 方法建立 View 。再次印證了第一部分得出的結論 createViewFromTag(parent, name, context, attrs)負責建立 View 物件

然後看下後面的程式碼我們就明白 inflate(@LayoutRes int resource, @Nullable ViewGroup root, boolean attachToRoot) 三個引數的關係了

1. root 不為 null 的時候,才會讀取 xml 跟佈局的 params 屬性。
   (這裡可以解釋為啥我們有時候用 LayoutInflater 載入的 xml 根標籤的屬性總是無效 )
2. attachToRoot 為 True ,返回的是  root 物件。否則返回的是 xml 建立的根標籤指定的 View
複製程式碼

LayoutInflater.createViewFromTag()建立 View 物件

通過上面的判斷我們終於找到了最最核心的方法 createViewFromTag()

private View createViewFromTag(View parent, String name, Context context, AttributeSet attrs) {
    return createViewFromTag(parent, name, context, attrs, false);
}
複製程式碼

有包裹了一層,並且把 ignoreThemeAttr 設定為 false,表示這裡會收到 Theme 的影響。我們在 createViewFromTag(parent, name, context, attrs, false) 中找到了建立 View 的程式碼

  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;
複製程式碼

這裡又出現了 mFactory2mFactorymPrivateFactory 三個物件,似乎都是可以建立 View 。 對於android.app.Activity來說,這三個物件為 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;
 }
複製程式碼

這裡需要說明一下,如果 name屬性裡面含有 . 表示這是一個自定義 View,因為只有自定義 View 才會把 View 的類路徑寫全。

對於自定義 View 的建立,這裡省略了大部分程式碼

public final View createView(String name, String prefix, AttributeSet attrs)
        throws ClassNotFoundException, InflateException {
    Constructor<? extends View> constructor = sConstructorMap.get(name);
    ……
    try {

        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;

    } ……
}
複製程式碼

僅僅看到 constructor.newInstance(args) 我們已經明白這裡使用了 反射建立 View 物件

而對於 Android 內建的各種 View 我們在 LayoutInflater 的實現類 PhoneLayoutInflater 中找到了過載

/**
 * @hide
 */
public class PhoneLayoutInflater extends LayoutInflater {
    private static final String[] sClassPrefixList = {
        "android.widget.",
        "android.webkit.",
        "android.app."
    };

    @Override protected View onCreateView(String name, AttributeSet attrs) throws ClassNotFoundException {
        for (String prefix : sClassPrefixList) {
            try {
                View view = createView(name, prefix, attrs);
                if (view != null) {
                    return view;
                }
            } catch (ClassNotFoundException e) {
                // In this case we want to let the base class take a crack
                // at it.
            }
        }

        return super.onCreateView(name, attrs);
    }
}
複製程式碼

再看下 LayoutInflater 中的程式碼

protected View onCreateView(View parent, String name, AttributeSet attrs)
        throws ClassNotFoundException {
    return onCreateView(name, attrs);
}

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

我們可以看到, 對於系統內建的 View,會依次在 View 的標籤前面加上"android.widget."、"android.webkit.","android.app." 、"android.view." 然後通過反射的方法建立 View。

最後補充一點,Activity 和 mFactory2mFactorymPrivateFactory 的關係

我們前面說過 對於android.app.Activity來說,mFactory2mFactorymPrivateFactory這三個物件為 null或者空實現

我們回到 Activity 的原始碼中

final void attach(……) {
    ……

    mWindow = new PhoneWindow(this, window, activityConfigCallback);
    mWindow.setWindowControllerCallback(this);
    mWindow.setCallback(this);
    mWindow.setOnWindowDismissedCallback(this);
    mWindow.getLayoutInflater().setPrivateFactory(this);
    ……
}
複製程式碼

這裡省略了很多程式碼,但是我們看到在建立完 PhoneWindow 以後,緊接著呼叫了mWindow.getLayoutInflater().setPrivateFactory(this)

這裡看到 Activity 實現了 LayoutInflater.Factory2 介面,並且通過mWindow.getLayoutInflater().setPrivateFactory(this),把 Activity 設定為 LayoutInflater 的 mPrivateFactory 成員變數。

public View onCreateView(View parent, String name, Context context, AttributeSet attrs) {
    if (!"fragment".equals(name)) {
        return onCreateView(name, context, attrs);
    }

    return mFragments.onCreateView(parent, name, context, attrs);
}

@Nullable
public View onCreateView(String name, Context context, AttributeSet attrs) {
    return null;
}
複製程式碼

這裡可以看到 Activity 通過自己的實現的 LayoutInflater.Factory2 介面,增加了對fragment標籤的處理。

順便說一下 AppCompat 元件的安裝

android.support.v7 包中提供了一些列 AppCompatXXX 替代Android自帶的 XXX 。例如 android.view.View 被替代為android.support.v7.widget

第一步 AppCompatActivity.onCreate(@Nullable Bundle savedInstanceState)

@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
    final AppCompatDelegate delegate = getDelegate();
    delegate.installViewFactory();
    delegate.onCreate(savedInstanceState);
    ……
    super.onCreate(savedInstanceState);
}
複製程式碼

這裡可以看到 delegate.installViewFactory(),該方法的實現類在 android.support.v7.app.AppCompatDelegateImplV9

@Override
public void installViewFactory() {
    LayoutInflater layoutInflater = LayoutInflater.from(mContext);
    if (layoutInflater.getFactory() == null) {
        LayoutInflaterCompat.setFactory2(layoutInflater, this);
    } else {
        if (!(layoutInflater.getFactory2() instanceof AppCompatDelegateImplV9)) {
            Log.i(TAG, "The Activity's LayoutInflater already has a Factory installed"
                    + " so we can not install AppCompat's");
        }
    }
}
複製程式碼

這裡看到 LayoutInflaterCompat.setFactory2(layoutInflater, this) ,跟蹤下去

public static void setFactory2(
        @NonNull LayoutInflater inflater, @NonNull LayoutInflater.Factory2 factory) {
    IMPL.setFactory2(inflater, factory);
}


public void setFactory2(LayoutInflater inflater, LayoutInflater.Factory2 factory) {
        inflater.setFactory2(factory);

        final LayoutInflater.Factory f = inflater.getFactory();
        if (f instanceof LayoutInflater.Factory2) {
            // The merged factory is now set to getFactory(), but not getFactory2() (pre-v21).
            // We will now try and force set the merged factory to mFactory2
            forceSetFactory2(inflater, (LayoutInflater.Factory2) f);
        } else {
            // Else, we will force set the original wrapped Factory2
            forceSetFactory2(inflater, factory);
        }
    }
複製程式碼

這裡看到 inflater.setFactory2(factory) ,表示已經安裝 AppCompatDelegateImplV9LayoutInflater.mFactory2

然後看 AppCompatDelegateImplV9.(View parent, final String name, @NonNull Context context, @NonNull AttributeSet attrs)

@Override
public View createView(View parent, final String name, @NonNull Context context, @NonNull AttributeSet attrs) {
    if (mAppCompatViewInflater == null) {
        mAppCompatViewInflater = new AppCompatViewInflater();
    }

    boolean inheritContext = false;
    if (IS_PRE_LOLLIPOP) {
        inheritContext = (attrs instanceof XmlPullParser)
                // If we have a XmlPullParser, we can detect where we are in the layout
                ? ((XmlPullParser) attrs).getDepth() > 1
                // Otherwise we have to use the old heuristic
                : shouldInheritContext((ViewParent) parent);
    }

    return mAppCompatViewInflater.createView(parent, name, context, attrs, inheritContext,
            IS_PRE_LOLLIPOP, /* Only read android:theme pre-L (L+ handles this anyway) */
            true, /* Read read app:theme as a fallback at all times for legacy reasons */
            VectorEnabledTintResources.shouldBeUsed() /* Only tint wrap the context if enabled */
    );
}
複製程式碼

最後到 AppCompatViewInflater.createView(View parent, final String name, @NonNull Context context, @NonNull AttributeSet attrs, boolean inheritContext, boolean readAndroidTheme, boolean readAppTheme, boolean wrapContext)

public final View createView(……) {
    ……
    // We need to 'inject' our tint aware Views in place of the standard framework versions
    switch (name) {
        case "TextView":
            view = new AppCompatTextView(context, attrs);
            break;
        ……
    }

    if (view == null && originalContext != context) {
        ……
        view = createViewFromTag(context, name, attrs);
    }

    if (view != null) {
        // If we have created a view, check its android:onClick
        checkOnClickListener(view, attrs);
    }

    return view;
}
複製程式碼

到此 ,Xml 檔案到 View 物件的轉換過程全部結束

end.jpg

參考資料

PhoneWindow.java

Android Context完全解析,你所不知道的Context的各種細節

相關文章