Android佈局優化之ViewStub、include、merge使用與原始碼分析
在開發中UI佈局是我們都會遇到的問題,隨著UI越來越多,佈局的重複性、複雜度也會隨之增長。Android官方給了幾個優化的方法,但是網路上的資料基本上都是對官方資料的翻譯,這些資料都特別的簡單,經常會出現問題而不知其所以然。這篇文章就是對這些問題的更詳細的說明,也歡迎大家多留言交流。
一、include
首先用得最多的應該是include,按照官方的意思,include就是為了解決重複定義相同佈局的問題。例如你有五個介面,這五個介面的頂部都有佈局一模一樣的一個返回按鈕和一個文字控制元件,在不使用include的情況下你在每個介面都需要重新在xml裡面寫同樣的返回按鈕和文字控制元件的頂部欄,這樣的重複工作會相當的噁心。使用include標籤,我們只需要把這個會被多次使用的頂部欄獨立成一個xml檔案,然後在需要使用的地方通過include標籤引入即可。其實就相當於C語言、C++中的include標頭檔案一樣,我們把一些常用的、底層的API封裝起來,然後複用,需要的時候引入它即可,而不必每次都自己寫一遍。示例如下 :
my_title_layout.xml
<?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" > <ImageButton android:id="@+id/back_btn" android:layout_width="wrap_content" android:layout_height="wrap_content" android:src="@drawable/ic_launcher" /> <TextView android:id="@+id/title_tv" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_centerVertical="true" android:layout_marginLeft="20dp" android:layout_toRightOf="@+id/back_btn" android:gravity="center" android:text="我的title" android:textSize="18sp" /> </RelativeLayout>
include佈局檔案:
<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical" > <include android:id="@+id/my_title_ly" android:layout_width="match_parent" android:layout_height="wrap_content" layout="@layout/my_title_layout" /> <!-- 程式碼省略 --> </LinearLayout>
這樣我們就可以使用my_title_layout了。
注意事項
使用include最常見的問題就是findViewById查詢不到目標控制元件,其正確的使用形式如下:
View titleView = findViewById(R.id.my_title_ly) ; TextView titleTextView = (TextView)titleView.findViewById(R.id.title_tv) ; titleTextView.setText("new Title");
首先找到include的id, 例如這裡include設定的id為“my_title_ly”,然後再對獲取到的titleView.findViewById來查詢目標佈局中的子控制元件,例如title_tv就是my_title_layout.xml中定義的子控制元件。因此我們如果需要查詢控制元件的話,可以設定include標籤的id,通過這個id獲取include對應的view以後,再通過對這個view進行findViewById才能正確查詢。如果你設定了include標籤的id,然後通過被include的佈局的root view的id來查詢子元素的話,則會報錯,如下 :
View titleView = findViewById(R.id.my_title_parent_id) ; TextView titleTextView = (TextView)titleView.findViewById(R.id.title_tv) ; titleTextView.setText("new Title");
這樣會報空指標異常,因為titleView沒有找到,會報空指標。那麼這是怎麼回事呢? 我們來分析它的原始碼看看吧。對於佈局檔案的解析,最終都會呼叫到LayoutInflater的inflate方法,該方法最終又會呼叫rInflate方法,我們看看這個方法。
/** * Recursive method used to descend down the xml hierarchy and instantiate * views, instantiate their children, and then call onFinishInflate(). */ void rInflate(XmlPullParser parser, View parent, final AttributeSet attrs, boolean finishInflate) throws XmlPullParserException, IOException { final int depth = parser.getDepth(); int type; // 迭代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(); if (TAG_REQUEST_FOCUS.equals(name)) { parseRequestFocus(parser, parent); } else if (TAG_INCLUDE.equals(name)) {// 如果xml中的節點是include節點,則呼叫parseInclude方法 if (parser.getDepth() == 0) { throw new InflateException("<include /> cannot be the root element"); } parseInclude(parser, parent, attrs); } else if (TAG_MERGE.equals(name)) { throw new InflateException("<merge /> must be the root element"); } else if (TAG_1995.equals(name)) { final View view = new BlinkLayout(mContext, attrs); final ViewGroup viewGroup = (ViewGroup) parent; final ViewGroup.LayoutParams params = viewGroup.generateLayoutParams(attrs); rInflate(parser, view, attrs, true); viewGroup.addView(view, params); } else { final View view = createViewFromTag(parent, name, attrs); final ViewGroup viewGroup = (ViewGroup) parent; final ViewGroup.LayoutParams params = viewGroup.generateLayoutParams(attrs); rInflate(parser, view, attrs, true); viewGroup.addView(view, params); } } if (finishInflate) parent.onFinishInflate(); }
這個方法其實就是遍歷xml中的所有元素,然後挨個進行解析。例如解析到一個標籤,那麼就根據使用者設定的一些layout_width、layout_height、id等屬性來構造一個TextView物件,然後新增到父控制元件(ViewGroup型別)中。標籤也是一樣的,我們看到遇到include標籤時,會呼叫parseInclude函式,這就是對標籤的解析,我們看看吧。
private void parseInclude(XmlPullParser parser, View parent, AttributeSet attrs) throws XmlPullParserException, IOException { int type; if (parent instanceof ViewGroup) { final int layout = attrs.getAttributeResourceValue(null, "layout", 0); if (layout == 0) {// include標籤中沒有設定layout屬性,會丟擲異常 final String value = attrs.getAttributeValue(null, "layout"); if (value == null) { throw new InflateException("You must specifiy a layout in the" + " include tag: <include layout=/"@layout/layoutID/" />"); } else { throw new InflateException("You must specifiy a valid layout " + "reference. The layout ID " + value + " is not valid."); } } else { final XmlResourceParser childParser = getContext().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)) { // Inflate all children. rInflate(childParser, parent, childAttrs, false); } else {// 2、我們例子中的情況會走到這一步,首先根據include的屬性集建立被include進來的xml佈局的根view // 這裡的根view對應為my_title_layout.xml中的RelativeLayout final View view = createViewFromTag(parent, childName, childAttrs); final ViewGroup group = (ViewGroup) parent;// include標籤的parent view ViewGroup.LayoutParams params = null; try {// 獲3、取佈局屬性 params = group.generateLayoutParams(attrs); } catch (RuntimeException e) { params = group.generateLayoutParams(childAttrs); } finally { if (params != null) {// 被inlcude進來的根view設定佈局引數 view.setLayoutParams(params); } } // 4、Inflate all children. 解析所有子控制元件 rInflate(childParser, view, childAttrs, true); // Attempt to override the included layout's android:id with the // one set on the <include /> tag itself. TypedArray a = mContext.obtainStyledAttributes(attrs, com.android.internal.R.styleable.View, 0, 0); int id = a.getResourceId(com.android.internal.R.styleable.View_id, View.NO_ID); // While we're at it, let's try to override android:visibility. int visibility = a.getInt(com.android.internal.R.styleable.View_visibility, -1); a.recycle(); // 5、將include中設定的id設定給根view,因此實際上my_title_layout.xml中的RelativeLayout的id會變成include標籤中的id,include不設定id,那麼也可以通過relative的找到. if (id != View.NO_ID) { 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"); } final int currentDepth = parser.getDepth(); while (((type = parser.next()) != XmlPullParser.END_TAG || parser.getDepth() > currentDepth) && type != XmlPullParser.END_DOCUMENT) { // Empty } }
整個過程就是根據不同的標籤解析不同的元素,首先會解析include元素,然後再解析被include進來的佈局的root view元素。在我們的例子中對應的root view就是id為my_title_parent_id的RelativeLayout,然後再解析root view下面的所有元素,這個過程是從上面註釋的2~4的過程,然後是設定佈局引數。我們注意看註釋5處,這裡就解釋了為什麼include標籤和被引入的佈局的根元素都設定了id的情況下,通過被引入的根元素的id來查詢子控制元件會找不到的情況。我們看到,註釋5處的會判斷include標籤的id如果不是View.NO_ID的話會把該id設定給被引入的佈局根元素的id,即此時在我們的例子中被引入的id為my_title_parent_id的根元素RelativeLayout的id被設定成了include標籤中的id,即RelativeLayout的id被動態修改成了”my_title_ly”。因此此時我們再通過“my_title_parent_id”這個id來查詢根元素就會找不到了!
所以結論就是: 如果include中設定了id,那麼就通過include的id來查詢被include佈局根元素的View;如果include中沒有設定Id, 而被include的佈局的根元素設定了id,那麼通過該根元素的id來查詢該view即可。拿到根元素後查詢其子控制元件都是一樣的。
二、ViewStub
我們先看看官方的說明:
ViewStub is a lightweight view with no dimension and doesn’t draw anything or participate in the layout. As such, it’s cheap to inflate and cheap to leave in a view hierarchy. Each ViewStub simply needs to include the android:layout attribute to specify the layout to inflate.
其實ViewStub就是一個寬高都為0的一個View,它預設是不可見的,只有通過呼叫setVisibility函式或者Inflate函式才會將其要裝載的目標佈局給載入出來,從而達到延遲載入的效果,這個要被載入的佈局通過android:layout屬性來設定。例如我們通過一個ViewStub來惰性載入一個訊息流的評論列表,因為一個帖子可能並沒有評論,此時我可以不載入這個評論的ListView,只有當有評論時我才把它載入出來,這樣就去除了載入ListView帶來的資源消耗以及延時,示例如下 :
<ViewStub android:id="@+id/stub_import" android:inflatedId="@+id/stub_comm_lv" android:layout="@layout/my_comment_layout" android:layout_width="fill_parent" android:layout_height="wrap_content" android:layout_gravity="bottom" /
my_comment_layout.xml如下:
<?xml version="1.0" encoding="utf-8"?> <ListView xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:id="@+id/my_comm_lv" android:layout_height="match_parent" > </ListView>
在執行時,我們只需要控制id為stub_import的ViewStub的可見性或者呼叫inflate()函式來控制是否載入這個評論列表即可。示例如下 :
public class MainActivity extends Activity { public void onCreate(Bundle b){ // main.xml中包含上面的ViewStub setContentView(R.layout.main); // 方式1,獲取ViewStub, ViewStub listStub = (ViewStub) findViewById(R.id.stub_import); // 載入評論列表佈局 listStub.setVisibility(View.VISIBLE); // 獲取到評論ListView,注意這裡是通過ViewStub的inflatedId來獲取 ListView commLv = findViewById(R.id.stub_comm_lv); if ( listStub.getVisibility() == View.VISIBLE ) { // 已經載入, 否則還沒有載入 } } }
通過setVisibility(View.VISIBILITY)來載入評論列表,此時你要獲取到評論ListView物件的話,則需要通過findViewById來查詢,而這個id並不是就是ViewStub的id。
這是為什麼呢 ?
我們先看ViewStub的部分程式碼吧:
@SuppressWarnings({"UnusedDeclaration"})
public ViewStub(Context context, AttributeSet attrs, int defStyle) {
TypedArray a = context.obtainStyledAttributes(attrs, com.android.internal.R.styleable.ViewStub,
defStyle, 0);
// 獲取inflatedId屬性
mInflatedId = a.getResourceId(R.styleable.ViewStub_inflatedId, NO_ID);
mLayoutResource = a.getResourceId(R.styleable.ViewStub_layout, 0);
a.recycle();
a = context.obtainStyledAttributes(attrs, com.android.internal.R.styleable.View, defStyle, 0);
mID = a.getResourceId(R.styleable.View_id, NO_ID);
a.recycle();
initialize(context);
}
private void initialize(Context context) {
mContext = context;
setVisibility(GONE);// 設定不可教案
setWillNotDraw(true);// 設定不繪製
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
setMeasuredDimension(0, 0);// 寬高都為0
}
@Override
public void setVisibility(int visibility) {
if (mInflatedViewRef != null) {// 如果已經載入過則只設定Visibility屬性
View view = mInflatedViewRef.get();
if (view != null) {
view.setVisibility(visibility);
} else {
throw new IllegalStateException("setVisibility called on un-referenced view");
}
} else {// 如果未載入,這載入目標佈局
super.setVisibility(visibility);
if (visibility == VISIBLE || visibility == INVISIBLE) {
inflate();// 呼叫inflate來載入目標佈局
}
}
}
/**
* Inflates the layout resource identified by {@link #getLayoutResource()}
* and replaces this StubbedView in its parent by the inflated layout resource.
*
* @return The inflated layout resource.
*
*/
public View inflate() {
final ViewParent viewParent = getParent();
if (viewParent != null && viewParent instanceof ViewGroup) {
if (mLayoutResource != 0) {
final ViewGroup parent = (ViewGroup) viewParent;// 獲取ViewStub的parent view,也是目標佈局根元素的parent view
final LayoutInflater factory = LayoutInflater.from(mContext);
final View view = factory.inflate(mLayoutResource, parent,
false);// 1、載入目標佈局
// 2、如果ViewStub的inflatedId不是NO_ID則把inflatedId設定為目標佈局根元素的id,即評論ListView的id
if (mInflatedId != NO_ID) {
view.setId(mInflatedId);
}
final int index = parent.indexOfChild(this);
parent.removeViewInLayout(this);// 3、將ViewStub自身從parent中移除
final ViewGroup.LayoutParams layoutParams = getLayoutParams();
if (layoutParams != null) {
parent.addView(view, index, layoutParams);// 4、將目標佈局的根元素新增到parent中,有引數
} else {
parent.addView(view, index);// 4、將目標佈局的根元素新增到parent中
}
mInflatedViewRef = new WeakReference<View>(view);
if (mInflateListener != null) {
mInflateListener.onInflate(this, view);
}
return view;
} else {
throw new IllegalArgumentException("ViewStub must have a valid layoutResource");
}
} else {
throw new IllegalStateException("ViewStub must have a non-null ViewGroup viewParent");
}
}
可以看到,其實最終載入目標佈局的還是inflate()函式,在該函式中將載入目標佈局,獲取到根元素後,如果mInflatedId不為NO_ID則把mInflatedId設定為根元素的id,這也是為什麼我們在獲取評論ListView時會使用findViewById(R.id.stub_comm_lv)來獲取,其中的stub_comm_lv就是ViewStub的inflatedId。當然如果你沒有設定inflatedId的話還是可以通過評論列表的id來獲取的,例如findViewById(R.id.my_comm_lv)。然後就是ViewStub從parent中移除、把目標佈局的根元素新增到parent中。最後會把目標佈局的根元素返回,因此我們在呼叫inflate()函式時可以直接獲得根元素,省掉了findViewById的過程。
還有一種方式載入目標佈局的就是直接呼叫ViewStub的inflate()方法,示例如下 :
public class MainActivity extends Activity { // 把commLv2設定為類的成員變數 ListView commLv2 = null; // public void onCreate(Bundle b){ // main.xml中包含上面的ViewStub setContentView(R.layout.main); // 方式二 ViewStub listStub2 = (ViewStub) findViewById(R.id.stub_import) ; // 成員變數commLv2為空則代表未載入 if ( commLv2 == null ) { // 載入評論列表佈局, 並且獲取評論ListView,inflate函式直接返回ListView物件 commLv2 = (ListView)listStub2.inflate(); } else { // ViewStub已經載入 } } }
注意事項
- 判斷是否已經載入過, 如果通過setVisibility來載入,那麼通過判斷可見性即可;如果通過inflate()來載入是不可以通過判斷可見性來處理的,而需要使用方式2來進行判斷。
- findViewById的問題,注意ViewStub中是否設定了inflatedId,如果設定了則需要通過inflatedId來查詢目標佈局的根元素。
三、Merge
首先我們看官方的說明:
The tag helps eliminate redundant view groups in your view hierarchy when including one layout within another. For example, if your main layout is a vertical LinearLayout in which two consecutive views can be re-used in multiple layouts, then the re-usable layout in which you place the two views requires its own root view. However, using another LinearLayout as the root for the re-usable layout would result in a vertical LinearLayout inside a vertical LinearLayout. The nested LinearLayout serves no real purpose other than to slow down your UI performance.
其實就是減少在include佈局檔案時的層級。標籤是這幾個標籤中最讓我費解的,大家可能想不到,標籤竟然會是一個Activity,裡面有一個LinearLayout物件。
/** * Exercise <merge /> tag in XML files. */ public class Merge extends Activity { private LinearLayout mLayout; @Override protected void onCreate(Bundle icicle) { super.onCreate(icicle); mLayout = new LinearLayout(this); mLayout.setOrientation(LinearLayout.VERTICAL); LayoutInflater.from(this).inflate(R.layout.merge_tag, mLayout); setContentView(mLayout); } public ViewGroup getLayout() { return mLayout; } }
使用merge來組織子元素可以減少佈局的層級。例如我們在複用一個含有多個子控制元件的佈局時,肯定需要一個ViewGroup來管理,例如這樣 :
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="fill_parent" android:layout_height="fill_parent"> <ImageView android:layout_width="fill_parent" android:layout_height="fill_parent" android:scaleType="center" android:src="@drawable/golden_gate" /> <TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginBottom="20dip" android:layout_gravity="center_horizontal|bottom" android:padding="12dip" android:background="#AA000000" android:textColor="#ffffffff" android:text="Golden Gate" /> </FrameLayout>
將該佈局通過include引入時就會多引入了一個FrameLayout層級,此時結構如下 :
使用merge標籤就會消除上圖中藍色的FrameLayout層級。示例如下 :
<merge xmlns:android="http://schemas.android.com/apk/res/android"> <ImageView android:layout_width="fill_parent" android:layout_height="fill_parent" android:scaleType="center" android:src="@drawable/golden_gate" /> <TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginBottom="20dip" android:layout_gravity="center_horizontal|bottom" android:padding="12dip" android:background="#AA000000" android:textColor="#ffffffff" android:text="Golden Gate" /> </merge>
效果圖如下 :
那麼它是如何實現的呢,我們還是看原始碼吧。相關的原始碼也是在LayoutInflater的inflate()函式中。
public View inflate(XmlPullParser parser, ViewGroup root, boolean attachToRoot) { synchronized (mConstructorArgs) { final AttributeSet attrs = Xml.asAttributeSet(parser); Context lastContext = (Context)mConstructorArgs[0]; mConstructorArgs[0] = mContext; View result = root; try { // Look for the root node. int type; while ((type = parser.next()) != XmlPullParser.START_TAG && type != XmlPullParser.END_DOCUMENT) { // Empty } if (type != XmlPullParser.START_TAG) { throw new InflateException(parser.getPositionDescription() + ": No start tag found!"); } final String name = parser.getName(); // m如果是erge標籤,那麼呼叫rInflate進行解析 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"); } // 解析merge標籤 rInflate(parser, root, attrs, false); } else { // 程式碼省略 } } catch (XmlPullParserException e) { // 程式碼省略 } return result; } } void rInflate(XmlPullParser parser, View parent, final AttributeSet attrs, boolean finishInflate) throws XmlPullParserException, IOException { final int depth = parser.getDepth(); int type; while (((type = parser.next()) != XmlPullParser.END_TAG || parser.getDepth() > depth) && type != XmlPullParser.END_DOCUMENT) { if (type != XmlPullParser.START_TAG) { continue; } final String name = parser.getName(); if (TAG_REQUEST_FOCUS.equals(name)) { parseRequestFocus(parser, parent); } else if (TAG_INCLUDE.equals(name)) { // 程式碼省略 parseInclude(parser, parent, attrs); } else if (TAG_MERGE.equals(name)) { throw new InflateException("<merge /> must be the root element"); } else if (TAG_1995.equals(name)) { final View view = new BlinkLayout(mContext, attrs); final ViewGroup viewGroup = (ViewGroup) parent; final ViewGroup.LayoutParams params = viewGroup.generateLayoutParams(attrs); rInflate(parser, view, attrs, true); viewGroup.addView(view, params); } else { // 我們的例子會進入這裡 final View view = createViewFromTag(parent, name, attrs); // 獲取merge標籤的parent final ViewGroup viewGroup = (ViewGroup) parent; // 獲取佈局引數 final ViewGroup.LayoutParams params = viewGroup.generateLayoutParams(attrs); // 遞迴解析每個子元素 rInflate(parser, view, attrs, true); // 將子元素直接新增到merge標籤的parent view中 viewGroup.addView(view, params); } } if (finishInflate) parent.onFinishInflate(); }
其實就是如果是merge標籤,那麼直接將其中的子元素新增到merge標籤parent中,這樣就保證了不會引入額外的層級。
在開發過程中,我們一定要儘量去深究一些常用技術點的本質,這樣才能避免出了問題不知如何解決的窘境。追根究底才能知道為什麼是這樣,也是自我成長的必經之路。
相關文章
- 佈局優化之ViewStub、Include、merge使用分析優化View
- Android佈局優化利器include和ViewStubAndroid優化View
- Android 佈局優化之includeAndroid優化
- Android優化之佈局優化Android優化
- Android效能優化之佈局優化Android優化
- Android 佈局優化Android優化
- Android 效能優化(二)之佈局優化面面觀Android優化
- Android學習之 UI佈局優化AndroidUI優化
- Android效能優——佈局優化Android優化
- Android佈局優化技巧Android優化
- Android 優化之路(一)佈局優化Android優化
- Android繪製優化(二)佈局優化Android優化
- Android佈局檢測優化Android優化
- Android中佈局的優化Android優化
- Android原始碼分析–ArrayMap優化Android原始碼優化
- Android——ConstraintLayout的使用,優化佈局效能AndroidAI優化
- Android佈局優化三劍客Android優化
- 佈局優化優化
- 轉:Android佈局優化三劍客Android優化
- 【Android原始碼】Activity如何載入佈局Android原始碼
- Android 原始碼分析之 AsyncTask 原始碼分析Android原始碼
- Element原始碼分析系列1一Layout(佈局)原始碼
- Merge、ViewStub標籤總結View
- Android 常用佈局 介紹與使用Android
- 11款Java工具:原始碼優化與分析Java原始碼優化
- Android效能優化之App應用啟動分析與優化Android優化APP
- Android最佳效能實踐(4):佈局優化技巧Android優化
- android 系統原始碼挖掘之Animator效能優化Android原始碼優化
- android筆記二(水平佈局與垂直佈局)Android筆記
- Android 中LayoutInflater(佈局載入器)原始碼篇之rInflate方法Android原始碼
- Android之TableLayout(表格佈局)Android
- Android之佈局屬性Android
- Android GUI之View佈局AndroidGUIView
- Element原始碼分析系列2-Container(佈局容器)原始碼AI
- Android 原始碼分析之 EventBus 的原始碼解析Android原始碼
- 原始碼解析Android中View的layout佈局過程原始碼AndroidView
- Android中View繪製優化二一---- 使用標籤複用佈局檔案AndroidView優化
- iOS Flexbox 佈局優化iOSFlex優化