多方位全面解析:如何正確地寫好一個介面

發表於2016-01-05

寫介面可以說是每位移動應用開發者的基本功,也是一位合格移動應用開發者繞不過去的坎。但就如不是每一位開發者都能夠成為合格的開發者一樣,本人在不同的團隊中發現,甚少有人能夠編寫出合格的UI程式碼;而非常奇怪的是,在很多的開發者論壇上看到我們移動開發者更多關注於某個控制元件或者是動畫,但卻很少看到深入剖析UI機制,指導UI開發的文章。

由於介面涉及到的方面實在過於廣泛,本文不可能事無鉅細,一一道來,所以本文先立足於點,深入剖析iOS UI系統中不被重視卻非常重要的機制,幫助本文讀者對iOS的UI系統有整體瞭解;進而以點帶面,擴充到UI邏輯設計和架構設計模式的討論;最後讀文而有所思有所得,設計開發出高效、易用、流暢的UI模組。

本文章節如下:

  1. 基礎與本質:說明普遍意義上的UI系統的三大模組,讓讀者從整體上對UI系統有清楚的認識。
  2. View:深入View的內部機制,View與Layer之間的關係,以及Offscreen Render;
  3. ViewController:講解ViewController在UI系統中所扮演的角色,以及UI架構設計中ViewController運用和實踐;
  4. MVC、MVP、MVVM:簡單分析三種主流的架構設計模式及其異同,並簡單提出了一些做架構設計意見和想法;
  5. 總結。

各章節間沒有必然的聯絡,讀者可以選擇感興趣章節閱讀。

1. 基礎與本質

終端App開發區別於後端開發最大的不同,就是終端開發很大部分的邏輯是為使用者提供介面以供人機互動,即所謂的UI(User Interface)。所以所有的UI架構主要關注三大模組:介面佈局管理,渲染及動畫、事件響應;

1.1 佈局管理

即在規定的座標系統上,按照一定的層級順序位置大小排布在容器內。一個UI系統必然有個基於座標的佈局管理系統,不管是Windows、Sysbian,還是Andorid、iOS。好的佈局管理機制直接影響介面邏輯實現的難易程度;

我們現在日常接觸到的App的UI座標系統都是二維的,我們現在玩的3D遊戲,受限於二維的展示螢幕,所以實質上只是三維在二維上的對映投影。我們一直在往更高的維度發展:全息影像、Hololens等等。在此可以設想下,未來我們構建介面的佈局管理很可能就是基於真實三維座標。

1.2 動畫及渲染

UI之所以叫User Interface,就是因為UI通過視覺上的展示,為使用者提供資訊。這些資訊的展示需要通過一系列複雜的計算,最後操作液晶體展示在螢幕上,這一系列過程就是渲染和動畫;

下圖就是應用介面渲染到展示的流程:

Render

引自WWDC2014 #419 Advanced Graphics and Animations for iOS Apps

這裡不展開來講,推薦沒看過的同學都認真觀看,能夠很好的理解渲染流程和介面優化;

推薦資料:

1.3 事件響應

UI除了展示資訊之外,還需要接收並響應使用者的點選、手勢、搖晃等事件,經過一系列操作後更新展示資訊,展示給使用者;正確及時地響應使用者的操作並給予反饋,是良好使用者體驗的保證。為何Android裝置普遍給人的感覺比iOS裝置要卡,其中一個主要的原因是iOS系統將響應使用者事件放在主執行緒的最高優先順序。

1.4 UI系統架構

從整體理解了上述三個方面,你會對UI架構有系統認識。iOS中的UI系統架構如下:

iOS UI Arch

引自WWDC2014 #419 Advanced Graphics and Animations for iOS Apps

2. View

UIView是UIKit中最基本控制元件,就如同NSObject基本上是Cocoa庫內所有類的基類一樣,UIView也是UIKit中所有介面控制元件的基類。只要你願意,你甚至只用UIView就可以搭建你的App(不過iOS9做了約束,必須設定keyWindow的rootViewControler)。

一般來說,熟練掌握常用的UIView子類控制元件(如UIButton, UIImageView, UILabel等)就足以應付90%的介面編碼需要。但想要編寫出高效、優美的介面程式碼,還需要更深入的瞭解。既然要深入,本文假設你對UIView已經有了初步的瞭解,至少使用寫過幾個完整的頁面;基於此設定下,本文討論聚焦於以下幾點:

1) UIView 與 CALayer:討論UIView背後的CALayer,瞭解CALayer與UIView的關係及渲染流程;

2) Offscreen Render:闡述什麼是Offscreen Render(離屏渲染),以及一些避免離屏渲染的方法;

3) UIResponser:討論UIView和UIViewController的父類UIResponser,分析iOS裝置上的事件響應鏈;

4) 設計與實踐:結合本人開發實踐經驗,說明在UIView應用中好的設計實踐規則;

參考:View Programming Guide for iOS

2.1 UIView 與 CALayer

我們應該都知道每個UIView都包含了一個CALayer,就算你沒直接看過CALayer,應該也使用過。比如給一個View切個圓角:view.layer.cornerRadius = 5.0f;;加個邊框:view.layer.borderWidth = 1.0f; view.layer.borderColor = [UIColor darkGrayColor].CGColor;,這裡使用的layer就是CALayer。

CALayer是QuartzCore庫內的類,是iOS上最基本的繪製單元;而UIView只是CALyer之上的封裝,更準確的來說,UIView是CALyer的簡版封裝,加上事件處理的集合類。事件處理我們下一節再討論,這裡的簡版封裝如何理解,為什麼不直接使用CALayer?

首先,如上一段所述,CALayer是最基本的繪製單元,每一個UIView都有一個CALayer的變數(public var layer: CALayer { get }),UIView的渲染實質就是這個layer的渲染。我們可以看看的類定義,裡面有很多屬性(變數)及方法在中可以找到幾乎一模一樣的對應;如屬性變數framehidden,方法public func convertPoint(p: CGPoint, fromLayer l: CALayer?) -> CGPoint等;但也有更多的屬性方法是UIView所沒有的,這裡就一一列舉了。我們可以看到UIView其實是把常用的介面(屬性和方法)暴露出來了,讓UIView更為易用。

其次,我們知道iOS平臺的Cocoa Touch 是源於OS X平臺的Cocoa),是在Cocoa的基礎上新增了適用於移動手機裝置的手勢識別、動畫等特性;但從底層實現上來說,Cocoa Touch與Cocoa共用一套底層的庫,其中就包括了QuartCore.framework;但QuartCore.framework一開始就是為OS X設計的,所以其中有部分特性是不適合做移動裝置開發的,比如最重要的座標系統。因此,我們也就不難理解為何UIView/NSView在CALayer上做了一層封裝。

以上,是UIView於CALayer的主要的關係。

2.2 Offscreen Render

當你尚在懵懂未知的開發初期,在寫UIScrollView及其子類(UITableView、UICollectionView)時,一定會遇到滾動不流暢,經常卡頓的情況;你認真研究程式碼,發現你邏輯程式碼都放到了非同步執行緒,主執行緒做的都是渲染介面的活,為什麼會卡頓?然後你想老手尋求幫助,老手會讓你去掉圓角、半透明和陰影之類,App又重回絲般順滑;你不知道為什麼,問老手,他可能會很詳細跟你解釋一通,然後你一知半解地點點頭,腦中一片茫然;較好的情況,也許你依稀記得這麼一個詞:離屏渲染(Offscreen Render)。那到底什麼是Offscreen Render?為什麼Offscreen Render會導致卡頓?

在第一章的1.2節中有提到渲染的流程圖,我們再更深入點,先看看最基本的渲染通道流程:

iOS UI Arch

引自WWDC2014 #419 Advanced Graphics and Animations for iOS Apps

我們再來看看需要Offscreen Render的渲染通道流程:

iOS UI Arch

引自WWDC2014 #419 Advanced Graphics and Animations for iOS Apps

一般情況下,OpenGL會將應用提交到Render Server的動畫直接渲染顯示(基本的Tile-Based渲染流程),但對於一些複雜的影象動畫的渲染並不能直接渲染疊加顯示,而是需要根據Command Buffer分通道進行渲染之後再組合,這一組合過程中,就有些渲染通道是不會直接顯示的;對比基本渲染通道流程和Masking渲染通道流程圖,我們可以看到到Masking渲染需要更多渲染通道和合並的步驟;而這些沒有直接顯示在螢幕的上的通道(如上圖的 Pass 1 和 Pass 2)就是Offscreen Rendering Pass。

Offscreen Render為什麼卡頓,從上圖我們就可以知道,Offscreen Render需要更多的渲染通道,而且不同的渲染通道間切換需要耗費一定的時間,這個時間內GPU會閒置,當通道達到一定數量,對效能也會有較大的影響;

那哪些情況會Offscreen Render呢?

注:layer.cornerRadius,layer.borderWidth,layer.borderColor並不會Offscreen Render,因為這些不需要加入Mask。

還有更多與Offscreen Render以及動畫圖形優化相關的知識,請認真觀看WWDC。

參考:

2.3 設計與實踐

以上幾節,對View在開發過程中經常遇到,但並不容易深入理解的概念進行了討論。接下來,我想脫離View的具體概念,談談本人在View設計和開發中的一些實踐經驗;

2.3.1 精簡扁平的View層次結構

複雜的View層次結果不僅會影響渲染效率,而且也會造成程式碼的臃腫,會造成不可預料的問題並且難以定位;怎麼樣維護一個精簡扁平的View層次結構呢?原則如下:

1) 儘量使用系統原生的控制元件;

如實現一個icon跟title上下佈局的按鈕,很多人習慣是使用一個view包含了一個UIButton和一個UILabel。實際上更為推薦的方式是調整UIButon的contentInset/titleInset/imageInset三個引數來達到這個效果,非常簡單,並且title有UIButton上的展示方式和特性,如可以設定高亮顏色等;

又比如一個有著複雜一點佈局結構的滾動介面,有些開發者會覺得使用UITableView/UICollectionView實現會比較複雜,有些效果可能沒辦法達到,就用他們的基類UIScrollView來實現,自己造了一大套的輪子,程式碼可能也變得非常複雜;實際上根據我的經驗,通過重寫或者是內部屬性的調整是完全可以使用UITableView/UICollectionView來達到這個效果,畢竟UITableView/UICollectionView是UIScrollView的子類,功能不會減少,而會更加強大,並且我們還能利用已有的data source和delegate機制,實現設計上的解耦。

其他常見的還有UINavigationBar、UITabBar、UIToolBar等等;

2) 合理新增/刪除動態View;

有些View是動態的,就是偶爾顯示,偶爾隱藏。這類View有兩種處理方式:增刪,或者顯示/隱藏。沒有標準的答案,個人更推薦增刪的處理方式,即在有需要的時候新增到對應的ContainerView上,在不需要的時候將其刪除。這樣即可以與懶載入結合在一起,而且也能避免兩個動態View的相互影響,比如TableFooterView,或者是錯誤載入View。但這並不是唯一的方式,假如這個動態View所在的View層級比較簡單,並且需要動畫進行動態展示,則使用顯示/隱藏也是不錯的處理方式。

2.3.2 通用控制元件;

每一個程式設計師都可以建立自己的程式碼庫,同理,每一位移動開發程式設計師都可以建立自己的通用控制元件程式碼庫。這個庫內的控制元件,可以是你自己寫的,也可以是優秀的第三方開源控制元件。建立控制元件庫,除了能夠避免重新造輪子,大大提高我們的開發效率,還有更為重要的一點:在運用、改造、重構中掌握介面設計解耦,甚至是架構的知識和經驗。

每個App的UI設計、互動、佈局和配色往往千差萬別,但總脫離不出移動App這一範疇,也就決定了在某些通用的控制元件互動上會保持一致性,以讓使用者依據自己在移動應用上的使用經驗就能輕鬆快速上手使用,這就是App的移動性。所以通用控制元件的適用場景往往是很“通用”的。比如下拉重新整理、載入更多、Tab Bar、提示Tips、載入錯誤重新載入等等。在新的App或者功能模組上運用這些控制元件時,你就會思考怎麼讓控制元件更加通用,即不影響舊的邏輯,又能夠適用新的需求,這對於做介面的架構設計是非常好的鍛鍊。

2.3.3 合理運用VC在替代View組合複雜介面;

在介面開發過程中,我們常常會遇到複雜的介面,比如多頁介面、多種佈局方式展示多業務的首頁等,但由於很大部分開發者已經對“一屏就是一個VC”這一初學者的習慣奉為教條,寫出一個龐然大View,再加上覆雜的邏輯程式碼,這一塊的程式碼很可能就演變成了誰都不敢動的禁區。一個VC可以管理多個VC,所以合理的使用VC來替代View進行復雜介面組合,不僅能夠將複雜介面切分成更小的粒度,邏輯程式碼也同步合理劃分,便於維護和重構;而依託VC的機制,還能View和資料的動態載入管理。

下一章中關於輕VC的討論是這一節知識的擴充。

3. ViewController

上一節關於View的章節已討論了iOS介面機制,這一節則主要是來談談在寫介面過程中的設計問題和基本規範;

ViewController在iOS只是一個非常重要的概念,它是我們在開發介面時最常打交道的模組,其在一個App中所扮演的角色,View Controller Programming Guide for iOS 中有清晰準確的描述:

1) View Management:管理View;

2) Data Marshalling:管理資料;

3) User Interactions:響應使用者互動;

4) Resource Management:管理資源;

5) Adaptivity:適配不同的螢幕尺寸空間的變化;

可以看到,ViewController有太多的事情要做,這也就導致了ViewController非常容易變得程式碼膨脹、邏輯混亂等問題;依照個人經驗,一個ViewController類的有效程式碼超過500行,這個ViewController就會變得難以維護,但實際上在開發過程中,往往會遇到上1K行,甚至2~3K行的ViewController類;當一個ViewController類達到2~3K行,就意味著其他開發者接手這個模組來修改東西,已經無法通過滾動來定位程式碼,只能通過搜尋;

所以,在進行介面開發時,ViewController需要特別注意模組設計,將不同的模組按照邏輯進行一定的拆分,即解耦,又防止ViewController模組的程式碼膨脹。這就是輕VC的理念;

3.1 輕VC

輕VC是前兩年非常火的名詞,現在似乎已經成為了一種業界規範或者是慣例。同上所述,一個VC的類,如果有效程式碼超過了500行,則表示這個類看是變得臃腫而難以維護;到達800行,只能通過搜尋來定位程式碼時,重構已勢在必行;

關於輕VC,objc.io的開篇第一章#Issue 1 : Lighter View Controllers,足見這一理念的重要性。掌握輕VC的理念基本上是一個iOS開發者從初級邁向高階必備技能。#Issue 1 : Lighter View Controllers 文中介紹了構建輕VC幾種常見的方式:

1) 將資料來源等複雜介面從VC中剝離;

2) 把業務邏輯程式碼抽象到Model層;

3) 將複雜View抽象成獨立的類;

4) 使用VC的Containment的特點,將一個VC中邏輯分離的介面模組剝離成為多個子VC;

想要設計出合理而易於理解和維護的輕VC結構,需要掌握輕VC的知識並有一定實踐經驗。在以下情況下,可以考慮將一個VC設計或者重構成更多模組更多類的輕VC結構:

1) 如上所述,程式碼超過500行時;

2) VC內的View的資料來源來自多個不同的地方;

3) VC內有多個複雜的View,需要展示資料實體類較為複雜;

總之,當你感覺你的VC已經變得臃腫,那麼就可嘗試輕VC的實踐,實踐才有收穫。

3.2 VC的設計

相對於View關注於佈局和展示,VC更關注設計和管理。本節以一個例項,來簡單介紹在一個完整App中的VC設計。

先來看一個常見的UI結構設計例子:

iOS UI Arch

這個圖應該非常容易理解:最底部是一個側滑抽屜控制元件,該抽屜包含了App內容展示的TabBarController和設定的VC;TabBarController的子Item VC包含了相應業務的List VC,點選List VC進入到詳情View內;有些詳情VC是使用WebViewController來進行內容的展示。非常簡單,不是麼?接下來說明該設計的洞見:

1) Root ViewController,是整個App內Window的根VC,這是一個生命週期與App相同的VC,即Window的RootViewController是唯一且一直存在的,需要切換場景則使用這個Root VC控制子VC切換來實現(常見於場景:需要進行強登入,即登入之後才能使用的App,登入成功後從登入介面切換到主介面,則登入VC和主介面VC都應該是Root VC的子VC,受Root VC的控制來進行切換)。這個RootViewController建議是一個UINavigationController,以此保證足夠擴充套件性,並提供更為豐富的介面互動選擇。這個Root VC的生命週期與App一致,這樣一些突發的靈活分支介面可以很好的展示在Root VC上,如全域性的Loading提示、OpenURL的分支調整等;

2) Main ViewController:主介面,是主要業務展示介面的根介面。該VC與RootVC功能上會很容易重合在一起,但需要注意的是,該VC並非一直存在,但切換到一些特定分支時,該VC會從Root VC上remove掉,比如前面所說的強登入App,登入介面與主介面就會需要進行切換。另外,該VC隔離了主要業務展示介面的VC與Root VC,便於App整體介面風格的改版和重構。比如現在上圖展示的是一個側滑抽屜+TabBar的組合,那到下個版本改版把側滑抽屜去掉,那麼只需要使用TabBar替換DrawerMenu VC在Main VC中的位置即可,而不會影響到RootVC中其他分支展示出來的介面(如Push等)。

3) TabBarItem ViewController:作為TabBar Controller的子Item VC,通常會設計為NavigationController,用以管理各TabBarItem內的VC棧。
注:如果需要在Push進入二級介面(Detail VC)時隱藏TabBar,只需要設定二級VC的hidesBottomBarWhenPushed = true即可,如果想更加靈活的控制TabBar,例如進到三級頁面的時候顯示出TabBar(這個場景應該很少見),或者你的TabBar是自定義的,可以參考我寫的一個開源控制元件MZNavTab

本節所示例的UI結構是一個非常通用的UI結構,市面上除遊戲外60%以上的App都是類似的UI互動(統計來源於個人手機),假如你的UI互動與此類似而你的UI結構很混亂的話,不如嘗試下這個UI結構設計。

4. MVC、MVP、MVVM

MVC

iOS UI Arch

MVP

iOS UI Arch

MVVM

iOS UI Arch

圖注:
虛線箭頭:表示兩者之間是非強依賴關係。如MVC圖,View與Model一般沒有直接聯絡。

虛線矩形:表示該模組在對應架構設計中的隱性存在。即一般性架構中並沒有這個角色,但立足於iOS這個平臺,這又是不可或缺的一部分;

本文並不打算將MVC、MVP、MVVM這個幾個通用架構設計模式的概念統統在這裡敘述一遍,上面三個圖基本上能夠很明白地對比出三者之間的差異。也許與你在網上看到的不盡相同,這是因為以上三圖更立足於iOS平臺。

4.1 MVC

我們最初看到的MVC設計模式圖可能是這樣的:

iOS UI Arch

引自[MSDN#ASP.NET – Single-Page Applications: Build Modern, Responsive Web Apps with ASP.NET(https://msdn.microsoft.com/en-us/magazine/dn463786.aspx)

而蘋果官方給的MVC的設計模式圖卻是這樣的:

iOS UI Arch

到底哪一副圖才是真正的MVC?我的答案只能是:都是。

MVC從施樂帕克實驗室提出至今,已經應用到各種應用開發領域中:Web App可以用MVC,iOS/Android/Windows客戶端應用也用MVC,Web前端也在用MVC,等等;這些幾乎涵蓋了我們常見的開發領域,所以MVC其實已經超越了他原本最初的設計,基於所有涉及展示的應用都能套上MVC,只不過不同的平臺在設計上略有差別。而MVP和MVVM,也不過是MVC的衍生變種,除這兩者之外,還有我們沒怎麼見過的HMVCMVA等。

4.2 Model Layer

在討論MVP和MVVM之前,我想先明確一個經常被誤解的概念:Model。由於Model這個詞太通用化,如資料Model,資料庫Model,這就導致了Model這一概念理解差異化,簡單的說,就是被玩壞。拋開其他,我們來看看常見的定義:

Wikipedia的定義

MSDN(https://msdn.microsoft.com/en-us/library/ff649643.aspx)中的定義

上面兩個定義基本一致:Model,管理應用的行為和資料。

再來看看Apple官方文件Model-View-Controller的定義

雖然Apple的官方文件是定義Model Objects,但它的含義還是封裝資料以及管理資料相關的邏輯計算;

所以這裡需要明確的一個概念是:在MVC的設計模式中,Model是一個Layer,而不只是一個資料模型(Data Model)類。總體來說,Model Layer 包含了資料模型,以及管理這些資料相關的邏輯計算,如本地資料變化、資料快取、從網路請求資料等業務邏輯。關於這個問題,還可以參考這篇文章:《iOS應用架構談 view層的組織和呼叫方案》。但有一點需要說明:該文章更傾向於從Model Object上思考Model的定義,因為裡面的關於Model的示例是從資料模型中擴充套件出業務介面;而本人則更傾向於從Model Layer來思考Model,即Model並不限於資料模型,可以是資料管理類(各種Manager)、請求佇列管理等等。

4.3 MVP VS MVVM

上一節關於Model Layer中推薦的文章《iOS應用架構談 view層的組織和呼叫方案》對MVC和MVVM都做了非常詳細的討論,是一篇非常不錯的文章,推薦各位閱讀,那麼本節就來說說MVP,以及我為什麼更傾向於選擇MVP作為App架構設計中的設計框架。

回顧下在本章一開始祭出的MVP以及MVVM兩張圖,兩者之間有什麼不同?

MVVM的VM(View Model)到V(View),比MVP的P(Presenter)到V(View),多了資料繫結。也就是

MVP:是MVC的變種,其中Model和View的定義與MVC的一致,不同點在於:MVC的Controller是管理一組Model與View之間互動邏輯,是一個管理者;而Presenter(展示者)則是Model於View之間的連線者,針對特定模組的View提供對應的格式化的Model資料,將View中的行為反饋到Model中。所以MVC中的Controller一般會管理一個或多個Model和一個或多個View,而Presenter則是 M-P-V 一對一,有更細的粒度和更好的解耦。

從MVP的定義,你會發現MVP與MVVM極其相似,Presenter與View Model扮演的角色基本沒有差別,除了前面所說到繫結機制。但繫結機制既有很明顯的強大優點——自動連線View和Model,也有很明顯的缺點——更高的耦合度,更復雜的程式碼邏輯;但讓人感嘆命運無常的是:MVVM隨著ReativeCocoa而在iOS平臺炙手可熱,而iOS平臺上甚少有人提及的MVP,在Android平臺卻幾乎成了標準(Android5.0引入了資料繫結支援,MVVM會在Android平臺有新的發展)。

我為什麼傾向於MVP?不過是相比於MVVM雙向繫結的便利,我更希望我的App設計中有更強的靈活性和擴充套件性。沒有完美的架構設計模式,只有適用於你的App業務場景和團隊的設計模式。比如資料邏輯並不複雜、更注重視覺展示的應用,原始的MVC往往是最優解。所有的MVC衍生出的變種,無非是為了Solve The Problem。

4.4 架構設計模式應用

無論MVC、MVP還是MVVM,都是指導我們進行架構設計的模式,並非可以生搬硬套的;而且在實際的應用中,對於這些設計模式總會有不同的理解,並且需要根據專案需求進行必要的調整;更為重要的是在我們App的架構設計中,處理好Model-View-Controller之間的關係只是基礎,最主要的挑戰來自於複雜的業務邏輯和場景,這才是體現一個架構師能力所在。

唐巧前不久寫的一篇文章《被誤解的MVC和被神化的MVVM》對MVC和MVVM的實踐的討論應該是體現了現在移動端主流架構思想,其中對網路請求層、ViewModel 層、Service 層、Storage 層等其它類的提取設計,才決定了一個App架構設計的優劣。

對於架構設計,我準備在下一篇文章,結合本人在iOS/Android兩端的設計經驗,做個深入的討論,並給出自己的設計範例,供各位討論參考。這裡先丟擲幾個在架構設計中最常思考的點,作為下一篇文章的引子:

1) 架構是為了解耦,越鬆的耦合就代表越多的份層,但人的思維總是更願意接受直線思維,怎麼解決這個矛盾?

2) 在一個App中,統一(一致)的架構設計能夠讓邏輯程式碼更健壯,更有利於團隊成員間的溝通和專案維護,但如何解決其和靈活性之間的矛盾?

3) 架構設計是否只包含邏輯分層?需要設計資料流和多執行緒麼?

4) 設計模式中的幾大原則;

5 總結

以上四個章節,先從UI整體出發,到剖析UIView幾點重要機制,接著討論怎麼用好VC這個UI中重要的管理角色,最後則漫談了MVC/MVVM/MVP幾個架構設計模式的異同和實踐應用,想通過以點帶面,讓我們在關注了具體實現之後,能夠脫離出來,從俯視下我們App開發更為整體核心的部分。

參考閱讀:

相關文章