Android走進Framework之AppCompatActivity.setContentView
前言
在上一篇Android走進Framework之app是如何被啟動的中講到了從我們點選app一直到呼叫Activity.onCreate()
的整個流程,今天來研究下我們最熟悉的一行程式碼setContentView()
。網上也有很多關於setContentView的原始碼解析,但是都是基於Activity
原始碼,而我們現在都是繼承的AppCompatActivity
,看原始碼發現改動還不少,所以我打算來研究下AppCompatActivity
裡是如何把我們的佈局新增進去的。你是否也曾有過同樣的疑惑,為什麼建立Activity
就要在onCreate()
裡面呼叫setContentView()
學前疑惑
setContentView
中到底做了什麼?為什麼我們呼叫後就可以顯示到我們想到的佈局?PhoneWindow
是個什麼鬼?Window
和它又有什麼關係?DecorView
什麼幹嘛的?和我們的佈局有什麼聯絡?- 在我們呼叫
requestFeature
的時候為什麼要在setContentView
之前?
接下來,我們就來解決這些疑惑!以下原始碼基於Api24
AppCompatActivity.setContentView
我們先來看下AppCompatActivity
中setContentView
AppCompatActivity.java
//這個是我們最常用的
@Override
public void setContentView(@LayoutRes int layoutResID) {
getDelegate().setContentView(layoutResID);
}
@Override
public void setContentView(View view) {
getDelegate().setContentView(view);
}
@Override
public void setContentView(View view, ViewGroup.LayoutParams params) {
getDelegate().setContentView(view, params);
}
可以看到3個過載的方法都呼叫getDelegate()
,而其他的方法也都是呼叫了getDelegate()
,很顯然這個是代理模式。那麼這個getDelegate()
返回的是什麼呢?
AppCompatActivity.java
/**
* @return The {@link AppCompatDelegate} being used by this Activity.
*/
@NonNull
public AppCompatDelegate getDelegate() {
if (mDelegate == null) {
//第一次為空,建立了AppCompatDelegate
mDelegate = AppCompatDelegate.create(this, this);
}
return mDelegate;
}
我們來看下AppCompatDelegate
是怎麼建立的
AppCompatDelegate.java
private static AppCompatDelegate create(Context context, Window window,
AppCompatCallback callback) {
final int sdk = Build.VERSION.SDK_INT;
if (BuildCompat.isAtLeastN()) {
//7.0以及7.0以上建立AppCompatDelegateImplN
return new AppCompatDelegateImplN(context, window, callback);
} else if (sdk >= 23) {
//6.0建立AppCompatDelegateImplV23
return new AppCompatDelegateImplV23(context, window, callback);
} else if (sdk >= 14) {
//...
return new AppCompatDelegateImplV14(context, window, callback);
} else if (sdk >= 11) {
//...
return new AppCompatDelegateImplV11(context, window, callback);
} else {
return new AppCompatDelegateImplV9(context, window, callback);
}
}
哦~原來根據不同的api版本返回不同的Delegate,我們先來看看AppCompatDelegateImplN
,裡面是否有setContentView
AppCompatDelegateImplN.java
@RequiresApi(24)
@TargetApi(24)
class AppCompatDelegateImplN extends AppCompatDelegateImplV23 {
AppCompatDelegateImplN(Context context, Window window, AppCompatCallback callback) {
super(context, window, callback);
}
@Override
Window.Callback wrapWindowCallback(Window.Callback callback) {
return new AppCompatWindowCallbackN(callback);
}
class AppCompatWindowCallbackN extends AppCompatWindowCallbackV23 {
AppCompatWindowCallbackN(Window.Callback callback) {
super(callback);
}
@Override
public void onProvideKeyboardShortcuts(
List<KeyboardShortcutGroup> data, Menu menu, int deviceId) {
final PanelFeatureState panel = getPanelState(Window.FEATURE_OPTIONS_PANEL, true);
if (panel != null && panel.menu != null) {
// The menu provided is one created by PhoneWindow which we don't actually use.
// Instead we'll pass through our own...
super.onProvideKeyboardShortcuts(data, panel.menu, deviceId);
} else {
// If we don't have a menu, jump pass through the original instead
super.onProvideKeyboardShortcuts(data, menu, deviceId);
}
}
}
}
發現並沒有setContentView
,那麼肯定在父類。誒,它繼承AppCompatDelegateImplV23
,而AppCompatDelegateImplV23
又繼承AppCompatDelegateImplV14
,AppCompatDelegateImplV14
又繼承AppCompatDelegateImplV11
,AppCompatDelegateImplV11
又繼承AppCompatDelegateImplV9
,好,知道關係後我有點懵逼了,搞什麼鬼?客官別急,我們先來畫個圖
ok,最後我在V9裡找到setContentView
,我們來看下
AppCompatDelegateImplV9.java
@Override
public void setContentView(int resId) {
//這個很關鍵,稍後會講
ensureSubDecor();
ViewGroup contentParent = (ViewGroup) mSubDecor.findViewById(android.R.id.content);
contentParent.removeAllViews();
//把我們的佈局放到contentParent裡面
LayoutInflater.from(mContext).inflate(resId, contentParent);
mOriginalWindowCallback.onContentChanged();
}
@Override
public void setContentView(View v) {
ensureSubDecor();
ViewGroup contentParent = (ViewGroup) mSubDecor.findViewById(android.R.id.content);
contentParent.removeAllViews();
contentParent.addView(v);
mOriginalWindowCallback.onContentChanged();
}
@Override
public void setContentView(View v, ViewGroup.LayoutParams lp) {
ensureSubDecor();
ViewGroup contentParent = (ViewGroup) mSubDecor.findViewById(android.R.id.content);
contentParent.removeAllViews();
contentParent.addView(v, lp);
mOriginalWindowCallback.onContentChanged();
}
這是對應的3個實現的方法,發現都會呼叫ensureSubDecor();
並且都會找到contentParent
,然後把我們的佈局放入進去
ok,到這裡我們來捋一捋流程。
private void ensureSubDecor() {
if (!mSubDecorInstalled) {
//這個mSubDecor其實就ViewGroup,呼叫createSubDecor()後,此時存放我們的佈局的容器已經準備好了
mSubDecor = createSubDecor();//核心程式碼!
// If a title was set before we installed the decor, propagate it now
CharSequence title = getTitle();
if (!TextUtils.isEmpty(title)) {
onTitleChanged(title);
}
applyFixedSizeWindow();
//SubDecor 安裝後的回撥
onSubDecorInstalled(mSubDecor);
//設定標記位
mSubDecorInstalled = true;
// Invalidate if the panel menu hasn't been created before this.
// Panel menu invalidation is deferred avoiding application onCreateOptionsMenu
// being called in the middle of onCreate or similar.
// A pending invalidation will typically be resolved before the posted message
// would run normally in order to satisfy instance state restoration.
PanelFeatureState st = getPanelState(FEATURE_OPTIONS_PANEL, false);
if (!isDestroyed() && (st == null || st.menu == null)) {
invalidatePanelMenu(FEATURE_SUPPORT_ACTION_BAR);
}
}
}
呼叫了createSubDecor()
,看字面意思建立了一個SubDecor
,看似跟DecorView
有聯絡。我們看下里面做了什麼操作
private ViewGroup createSubDecor() {
TypedArray a = mContext.obtainStyledAttributes(R.styleable.AppCompatTheme);
if (!a.hasValue(R.styleable.AppCompatTheme_windowActionBar)) {
a.recycle();
//還記得我們使用AppCompatActivity如果不設定AppCompat主題報的錯誤嗎?就是在這裡丟擲來的
throw new IllegalStateException(
"You need to use a Theme.AppCompat theme (or descendant) with this activity.");
}
//初始化相關特徵標誌
if (a.getBoolean(R.styleable.AppCompatTheme_windowNoTitle, false)) {
//一般我們的主題預設都是NoTitle
requestWindowFeature(Window.FEATURE_NO_TITLE);
} else if (a.getBoolean(R.styleable.AppCompatTheme_windowActionBar, false)) {
// Don't allow an action bar if there is no title.
requestWindowFeature(FEATURE_SUPPORT_ACTION_BAR);
}
if (a.getBoolean(R.styleable.AppCompatTheme_windowActionBarOverlay, false)) {
requestWindowFeature(FEATURE_SUPPORT_ACTION_BAR_OVERLAY);
}
if (a.getBoolean(R.styleable.AppCompatTheme_windowActionModeOverlay, false)) {
requestWindowFeature(FEATURE_ACTION_MODE_OVERLAY);
}
mIsFloating = a.getBoolean(R.styleable.AppCompatTheme_android_windowIsFloating, false);
a.recycle();
//重點!在這裡就建立DecorView,至於DecorView到底是什麼以及如何建立的,稍後會講到
mWindow.getDecorView();
final LayoutInflater inflater = LayoutInflater.from(mContext);
//可以看到其實就是個ViewGroup,我們接著往下看,跟DecorView到底有啥關係
ViewGroup subDecor = null;
if (!mWindowNoTitle) {
//上面說了主題預設都是NoTitle,所以不會走裡面的方法
if (mIsFloating) {
// If we're floating, inflate the dialog title decor
subDecor = (ViewGroup) inflater.inflate(
R.layout.abc_dialog_title_material, null);
...
} else if (mHasActionBar) {
TypedValue outValue = new TypedValue();
mContext.getTheme().resolveAttribute(R.attr.actionBarTheme, outValue, true);
...
// Now inflate the view using the themed context and set it as the content view
subDecor = (ViewGroup) LayoutInflater.from(themedContext)
.inflate(R.layout.abc_screen_toolbar, null);
/**
* Propagate features to DecorContentParent
*/
if (mOverlayActionBar) {
mDecorContentParent.initFeature(FEATURE_SUPPORT_ACTION_BAR_OVERLAY);
}
if (mFeatureProgress) {
mDecorContentParent.initFeature(Window.FEATURE_PROGRESS);
}
if (mFeatureIndeterminateProgress) {
mDecorContentParent.initFeature(Window.FEATURE_INDETERMINATE_PROGRESS);
}
}
} else {
//我們進入else
if (mOverlayActionMode) {
//呼叫了requestWindowFeature(FEATURE_ACTION_MODE_OVERLAY)會走進來
subDecor = (ViewGroup) inflater.inflate(
R.layout.abc_screen_simple_overlay_action_mode, null);
} else {
//ok,所以如果這些我們都沒設定,預設就走到這裡來了,在這裡映射出了subDecor,稍後我們來看下這個佈局是啥
subDecor = (ViewGroup) inflater.inflate(R.layout.abc_screen_simple, null);
}
...
}
if (subDecor == null) {
throw new IllegalArgumentException(
"AppCompat does not support the current theme features: { "
+ "windowActionBar: " + mHasActionBar
+ ", windowActionBarOverlay: "+ mOverlayActionBar
+ ", android:windowIsFloating: " + mIsFloating
+ ", windowActionModeOverlay: " + mOverlayActionMode
+ ", windowNoTitle: " + mWindowNoTitle
+ " }");
}
if (mDecorContentParent == null) {
mTitleView = (TextView) subDecor.findViewById(R.id.title);
}
// Make the decor optionally fit system windows, like the window's decor
ViewUtils.makeOptionalFitsSystemWindows(subDecor);
//這個contentView很重要,是我們佈局的父容器,你可以把它直接當成FrameLayout
final ContentFrameLayout contentView = (ContentFrameLayout) subDecor.findViewById(
R.id.action_bar_activity_content);
//看過相關知識的同學應該知道android.R.id.content這個Id在以前是我們佈局的父容器的Id
final ViewGroup windowContentView = (ViewGroup) mWindow.findViewById(android.R.id.content);
if (windowContentView != null) {
// There might be Views already added to the Window's content view so we need to
// migrate them to our content view
while (windowContentView.getChildCount() > 0) {
final View child = windowContentView.getChildAt(0);
windowContentView.removeViewAt(0);
contentView.addView(child);
}
//注意!原來windowContentView的Id是android.R.id.content,現在設定成NO_ID
windowContentView.setId(View.NO_ID);
//在之前這個id是我們的父容器,現在將contentView設定成android.R.id.content,那麼可以初步判定,這個contentView將會是我的父容器
contentView.setId(android.R.id.content);
// The decorContent may have a foreground drawable set (windowContentOverlay).
// Remove this as we handle it ourselves
if (windowContentView instanceof FrameLayout) {
((FrameLayout) windowContentView).setForeground(null);
}
}
// Now set the Window's content view with the decor
//注意!重要!將subDecor放入到了這個Window裡面,這個Window是個抽象類,其實現類是PhoneWindow,稍後會講到
mWindow.setContentView(subDecor);
....
return subDecor;
}
看到了requestWindowFeature是不是很熟悉?還記得我們是怎麼讓Activity全屏的嗎?
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
requestWindowFeature(Window.FEATURE_NO_TITLE);
getWindow().setFlags(WindowManager.LayoutParams.FILL_PARENT
,WindowManager.LayoutParams.FILL_PARENT);
setContentView(R.layout.activity_test);
}
而且這兩行程式碼必須在setContentView()之前呼叫,知道為啥了吧?因為在這裡就把Window的相關特徵標誌給初始化了,在setContentView()之後呼叫就不起作用了!
在程式碼裡其他比較重要的地方已寫了註釋,我們來看下這個abc_screen_simple.xml
的佈局到底是什麼樣子的
abc_screen_simple.xml
<android.support.v7.internal.widget.FitWindowsLinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/action_bar_root"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:fitsSystemWindows="true">
<android.support.v7.internal.widget.ViewStubCompat
android:id="@+id/action_mode_bar_stub"
android:inflatedId="@+id/action_mode_bar"
android:layout="@layout/abc_action_mode_bar"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
<include layout="@layout/abc_screen_content_include" />
</android.support.v7.internal.widget.FitWindowsLinearLayout>
abc_screen_content_include.xml
<merge xmlns:android="http://schemas.android.com/apk/res/android">
<android.support.v7.internal.widget.ContentFrameLayout
android:id="@id/action_bar_activity_content"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:foregroundGravity="fill_horizontal|top"
android:foreground="?android:attr/windowContentOverlay" />
</merge>
原來這個subDecor
就是FitWindowsLinearLayout
看到這2個佈局,我們先把這2個佈局用圖畫出來。
(圖不在美,能懂就行~)
從AppCompatActivity到現在佈局,在我的腦海裡浮現出這樣的的畫面。。。
那這是不是我們app最終的佈局呢?當然不是,因為我們還沒講到非常重要的兩行程式碼
mWindow.getDecorView();
mWindow.setContentView(subDecor);
註釋中說道Window是個抽象類,其實現類是PhoneWindow。那麼我們先來看PhoneWindow的getDecorView做了什麼
PhoneWindow.java
public class PhoneWindow extends Window implements MenuBuilder.Callback {
@Override
public final View getDecorView() {
if (mDecor == null || mForceDecorInstall) {
installDecor();
}
return mDecor;
}
private void installDecor() {
//mDecor是DecorView,第一次mDecor=null,所以呼叫generateDecor
if (mDecor == null) {
mDecor = generateDecor();
...
}
//第一次mContentParent也等於null
if (mContentParent == null) {
//可以看到把DecorView傳入進去了
mContentParent = generateLayout(mDecor);
}
}
}
在generateDecor()做了什麼?其實返回了一個DecorView物件。
protected DecorView generateDecor(int featureId) {
// System process doesn't have application context and in that case we need to directly use
// the context we have. Otherwise we want the application context, so we don't cling to the
// activity.
Context context;
if (mUseDecorContext) {
Context applicationContext = getContext().getApplicationContext();
if (applicationContext == null) {
context = getContext();
} else {
context = new DecorContext(applicationContext, getContext().getResources());
if (mTheme != -1) {
context.setTheme(mTheme);
}
}
} else {
context = getContext();
}
return new DecorView(context, featureId, this, getAttributes());
}
DecorView是啥呢?
public class DecorView extends FrameLayout implements RootViewSurfaceTaker, WindowCallbacks {
...
}
哦~原來繼承FrameLayout,起到了裝飾的作用。
我們在來看看generateLayout()
做了什麼。
protected ViewGroup generateLayout(DecorView decor) {
TypedArray a = getWindowStyle();
//設定一堆標誌位...
...
if (!mForcedStatusBarColor) {
//獲取主題狀態列的顏色
mStatusBarColor = a.getColor(R.styleable.Window_statusBarColor, 0xFF000000);
}
if (!mForcedNavigationBarColor) {
//獲取底部NavigationBar顏色
mNavigationBarColor = a.getColor(R.styleable.Window_navigationBarColor, 0xFF000000);
}
//獲取主題一些資源
...
// Inflate the window decor.
int layoutResource;
int features = getLocalFeatures();
// System.out.println("Features: 0x" + Integer.toHexString(features));
if ((features & (1 << FEATURE_SWIPE_TO_DISMISS)) != 0) {
...我們設定不同的主題以及樣式,會採用不同的佈局檔案...
} else {
//記住這個佈局,之後我們會來驗證下佈局的結構
layoutResource = R.layout.screen_simple;
// System.out.println("Simple!");
}
//要開始更改mDecor啦~
mDecor.startChanging();
//注意,此時把screen_simple放到了DecorView裡面
mDecor.onResourcesLoaded(mLayoutInflater, layoutResource);
//這裡的ID_ANDROID_CONTENT就是R.id.content;
ViewGroup contentParent = (ViewGroup)findViewById(ID_ANDROID_CONTENT);
if (contentParent == null) {
throw new RuntimeException("Window couldn't find content container view");
}
...
//這裡的getContainer()返回的是個Window類,也就是父Window,一般為空
if (getContainer() == null) {
final Drawable background;
if (mBackgroundResource != 0) {
background = getContext().getDrawable(mBackgroundResource);
} else {
background = mBackgroundDrawable;
}
//設定背景
mDecor.setWindowBackground(background);
final Drawable frame;
if (mFrameResource != 0) {
frame = getContext().getDrawable(mFrameResource);
} else {
frame = null;
}
mDecor.setWindowFrame(frame);
mDecor.setElevation(mElevation);
mDecor.setClipToOutline(mClipToOutline);
if (mTitle != null) {
setTitle(mTitle);
}
if (mTitleColor == 0) {
mTitleColor = mTextColor;
}
setTitleColor(mTitleColor);
}
mDecor.finishChanging();
return contentParent;
}
可以看到根據不同主題屬性使用的不同的佈局,然後返回了這個佈局contentParent
。
我們來看看這個screen_simple.xml佈局是什麼樣子的
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fitsSystemWindows="true"
android:orientation="vertical">
<ViewStub android:id="@+id/action_mode_bar_stub"
android:inflatedId="@+id/action_mode_bar"
android:layout="@layout/action_mode_bar"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
<FrameLayout
android:id="@android:id/content"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:foregroundInsidePadding="false"
android:foregroundGravity="fill_horizontal|top"
android:foreground="?android:attr/windowContentOverlay" />
</LinearLayout>
咦,這個佈局結構跟subDecor
好相似啊。。
好了,到目前為止我們知道了,當我們呼叫mWindow.getDecorView();
的時候裡面建立DecorView,然後又根據不同主題屬性新增不同佈局放到DecorView下,然後找到這個佈局的R.id.content
,也就是mContentParent
。ok,搞清楚mWindow.getDecorView();
之後,我們在來看看mWindow.setContentView(subDecor);
(注意:此時把subDecor傳入進去)
@Override
public void setContentView(View view) {
//呼叫下面的過載方法
setContentView(view, new ViewGroup.LayoutParams(MATCH_PARENT, MATCH_PARENT));
}
@Override
public void setContentView(View view, ViewGroup.LayoutParams params) {
//在mWindow.getDecorView()已經建立了mContentParent
if (mContentParent == null) {
installDecor();
} else if (!hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
mContentParent.removeAllViews();
}
//是否有transitions動畫。沒有,進入else
if (hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
view.setLayoutParams(params);
final Scene newScene = new Scene(mContentParent, view);
transitionTo(newScene);
} else {
//重要!!將這個subDecor也就是FitWindowsLinearLayout新增到這個mContentParent裡面了
//mContentParent是FrameLayout,在之前設定的View.NO_ID
mContentParent.addView(view, params);
}
mContentParent.requestApplyInsets();
final Callback cb = getCallback();
if (cb != null && !isDestroyed()) {
cb.onContentChanged();
}
mContentParentExplicitlySet = true;
}
當呼叫了mWindow.getDecorView();
建立了DecorView以及mContentParent
,並且把subDecor
放到了mContentParent
裡面。我們再來回頭看看AppCompatDelegateImplV9
,還記得它嗎?當我們在AppCompatActivity
的setContentView
的時候會去呼叫AppCompatDelegateImplV9
的setContentView
AppCompatDelegateImplV9.java
@Override
public void setContentView(View v) {
//此時DecorView和subDecor都建立好了
ensureSubDecor();
//還記得呼叫createSubDecor的時候把原本是R.id.content的windowContentView設定成了NO_ID,並且將contentView也就是ContentFrameLayout設定成了R.id.content嗎?也就是說此時的contentParent就是ContentFrameLayout
ViewGroup contentParent = (ViewGroup) mSubDecor.findViewById(android.R.id.content);
contentParent.removeAllViews();
//將我的佈局放到contentParent裡面
contentParent.addView(v);
mOriginalWindowCallback.onContentChanged();
}
@Override
public void setContentView(int resId) {
ensureSubDecor();
ViewGroup contentParent = (ViewGroup) mSubDecor.findViewById(android.R.id.content);
contentParent.removeAllViews();
//將我們的佈局id對映成View並且放到contentParent下
LayoutInflater.from(mContext).inflate(resId, contentParent);
mOriginalWindowCallback.onContentChanged();
}
@Override
public void setContentView(View v, ViewGroup.LayoutParams lp) {
ensureSubDecor();
ViewGroup contentParent = (ViewGroup) mSubDecor.findViewById(android.R.id.content);
contentParent.removeAllViews();
contentParent.addView(v, lp);
mOriginalWindowCallback.onContentChanged();
}
完整佈局
ok,看到這裡,想必大家在腦海裡也有個大致佈局了吧,我們再來把整個app初始佈局畫出來
驗證佈局
接下來我們來驗證下我們佈局結構是否正確
新建一個Activity
public class TestAcitivty extends AppCompatActivity {
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_test);
}
}
主題
<!-- Base application theme. -->
<style name="AppTheme" parent="Theme.AppCompat.Light.NoActionBar">
<!-- Customize your theme here. -->
<item name="colorPrimary">@color/colorPrimary</item>
<item name="colorPrimaryDark">@color/colorPrimaryDark</item>
<item name="colorAccent">@color/colorAccent</item>
<item name="android:listDivider">@color/divider_dddddd</item>
</style>
為了演示佈局非常簡單,就是一個textview
<?xml version="1.0" encoding="utf-8"?>
<TextView xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/textView"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
</TextView>
執行後,我們在用hierarchyviewer
檢視下
看來我們的腦補的佈局是對的!
學後總結
整個流程就是這樣。看到這裡我們明白了,當我們呼叫setContentView
的時候載入了2次系統佈局,在PhoneWindow
裡面建立了DecorView
,DecorView
是我們的最底層的View,並且將我們的佈局放入到一個ContentFrameLayout
裡,我們還知道在setContentView
的時候進行了相關特徵標誌初始化,所以在它之後呼叫requestWindowFeature
就會不起作用然後報錯。
setContentView時序圖
知道這些之後我們不妨用時序圖來梳理下整個呼叫的流程
致謝
結語
當然,在這篇文章中,因為篇幅問題,也有許多沒有講的重要知識點,比如:
PhoneWindow
在哪裡初始化?它做了哪些事?view
樹是如何被管理的?findViewById
到底是怎麼找到對應的View的?- 為什麼說
setContentView
在onResume
在對使用者可見? - 等等…
在下一篇中我會詳細講解這些問題。
最後
文中如有錯誤,希望大家指出!