Follow GitHub: Draveness
這是談談 MVX 系列的第二篇文章,上一篇文章中對 iOS 中 Model 層的設計進行了簡要的分析;而在這里语泽,我們會對 MVC 中的視圖層進行討論,談一談現有的視圖層有著什么樣的問題刽漂,如何在框架的層面上去改進僻焚,同時與服務端的視圖層進行對比,分析它們的差異衅疙。
UIKit
UIKit 是 Cocoa Touch 中用于構建和管理應用的用戶界面的框架莲趣,其中幾乎包含著與 UI 相關的全部功能,而我們今天想要介紹的其實是 UIKit 中與視圖相關的一部分饱溢,也就是 UIView
以及相關類喧伞。
UIView
可以說是 iOS 中用于渲染和展示內容的最小單元,作為開發(fā)者能夠接觸到的大多數屬性和方法也都由 UIView
所提供,比如最基本的布局方式 frame 就是通過 UIView
的屬性所控制潘鲫,在 Cocoa Touch 中的所有布局系統(tǒng)最終都會轉化為 CFRect 并通過 frame 的方式完成最終的布局翁逞。
UIView
作為 UIKit 中極為重要的類,它的 API 以及設計理念決定了整個 iOS 的視圖層該如何工作溉仑,這也是理解視圖層之前必須要先理解 UIView
的原因挖函。
UIView
在 UIKit 中,除了極少數用于展示的類不繼承自 UIView
之外浊竟,幾乎所有類的父類或者或者祖先鏈中一定會存在 UIView
怨喘。
我們暫且拋開不繼承自 UIView
的 UIBarItem
類簇不提,先通過一段代碼分析一下 UIView
具有哪些特性振定。
UIImageView *backgroundView = [[UIImageView alloc] initWithImage:[UIImage imageNamed:@"backgoundImage"]];
UIImageView *logoView = [[UIImageView alloc] initWithImage:[UIImage imageNamed:@"logo"]];
UIButton *loginButton = [[UIButton alloc] init];
[loginButton setTitle:@"登錄" forState:UIControlStateNormal];
[loginButton setTitleColor:UIColorFromRGB(0xFFFFFF) forState:UIControlStateNormal];
[loginButton.titleLabel setFont:[UIFont boldSystemFontOfSize:18]];
[loginButton setBackgroundColor:UIColorFromRGB(0x00C3F3)];
[self.view addSubview:backgroundView];
[backgroundView addSubview:logoView];
[backgroundView addSubview:loginButton];
UIView
作為視圖層大部分元素的根類必怜,提供了兩個非常重要的特性:
由于
UIView
具有frame
屬性,所以為所有繼承自UIView
的類提供了絕對布局相關的功能后频,也就是在 Cocoa Touch 中梳庆,所有的視圖元素都可以通過frame
設置自己在父視圖中的絕對布局;-
UIView
在接口中提供了操作和管理視圖層級的屬性和方法卑惜,比如superview
靠益、subviews
以及-addSubview:
等方法;@interface UIView (UIViewHierarchy) @property (nullable, nonatomic, readonly) UIView *superview; @property (nonatomic, readonly, copy) NSArray<__kindof UIView *> *subviews; - (void)addSubview:(UIView *)view; ... @end
也就是說 UIView 和它所有的子類都可以擁有子視圖残揉,成為容器并包含其他 UIView 的實例胧后。
[self.view addSubview:backgroundView]; [backgroundView addSubview:logoView]; [backgroundView addSubview:loginButton];
這種使用 UIView
同時為子類提供默認的 frame
布局以及子視圖支持的方式在一定程度上能夠降低視圖模型的復雜度:因為所有的視圖都是一個容器,所以在開發(fā)時不需要區(qū)分視圖和容器抱环,但是這種方式雖然帶來了一些方便壳快,但是也不可避免地帶來了一些問題。
UIView 與布局
在早期的 Cocoa Touch 中镇草,整個視圖層的布局都只是通過 frame
屬性來完成的(絕對布局)眶痰,一方面是因為在 iPhone5 之前,iOS 應用需要適配的屏幕尺寸非常單一梯啤,完全沒有適配的兼容問題竖伯,所以使用單一的 frame
布局方式完全是可行的。
但是在目前各種屏幕尺寸的種類暴增的情況下因宇,就很難使用 frame
對所有的屏幕進行適配七婴,在這時蘋果就引入了 Auto Layout 采用相對距離為視圖層的元素進行布局。
不過察滑,這算是蘋果比較失敗的一次性嘗試打厘,主要是因為使用 Auto Layout 對視圖進行布局實在太過復雜,所以剛出來的時候也不溫不火贺辰,很少有人使用户盯,直到 Masonry 的出現使得編寫 Auto Layout 代碼沒有那么麻煩和痛苦才普及起來嵌施。
但是由于 Auto Layout 的工作原理實際上是解 N 元一次方程組,所以在遇到復雜視圖時莽鸭,會遇到非常嚴重的性能問題吗伤,如果想要了解相關的問題的話,可以閱讀 從 Auto Layout 的布局算法談性能 這篇文章硫眨,在這里就不再贅述了牲芋。
然而 Auto Layout 的相對布局雖然能夠在一定程度上解決適配屏幕大小和尺寸接近的適配問題,比如 iPhone4s捺球、iPhone5、iPhone6 Plus 等移動設備夕冲,或者iPad 等平板設備氮兵。但是,Auto Layout 不能通過一套代碼打通 iPhone 和 iPad 之間布局方式的差異歹鱼,只能通過代碼中的 if 和 else 進行判斷泣栈。
在這種背景下,蘋果做了很多的嘗試弥姻,比如說 Size-Class-Specific Layout南片,Size Class 將屏幕的長寬分為三種:
- Compact
- Regular
- Any
這樣就出現了最多 3 x 3 的組合,比如屏幕寬度為 Compact 高度為 Regular 等等庭敦,它與 Auto Layout 一起工作省去了一些 if 和 else 的條件判斷疼进,但是從實際效果上來說,它的用處并不是特別大秧廉,而且使用代碼來做 Size Class 的相關工作依然非常困難伞广。
除了 Auto Layout 和 Size Class 之外,蘋果在 iOS9 還推出了 UIStackView
來增加 iOS 中的布局方式和手段疼电,這是一種類似 flexbox 的布局方式嚼锄。
雖然 UIStackView
可以起到一定的作用,但是由于大多數 iOS 應用都要求對設計稿進行嚴格還原并且其 API 設計相對啰嗦蔽豺,開發(fā)者同時也習慣了使用 Auto Layout 的開發(fā)方式区丑,在慣性的驅動下,UIStackView
應用的也不是非常廣泛修陡。
不過現在很多跨平臺的框架都是用類似 UIStackView
的方式進行布局沧侥,比如 React Native、Weex 等魄鸦,其內部都使用 Facebook 開源的 Yoga正什。
由于 flexbox 以及類似的布局方式在其他平臺上都有類似的實現,并且其應用確實非常廣泛号杏,筆者認為隨著工具的完善婴氮,這種布局方式會逐漸進入 iOS 開發(fā)者的工具箱中斯棒。
三種布局方式 frame
、Auto Layout 以及 UIStackView
其實最終布局都會使用 frame
主经,其他兩種方式 Auto Layout 和 UIStackView
都會將代碼描述的布局轉換成 frame
進行荣暮。
布局機制的混用
Auto Layout 和 UIStackView
的出現雖然為布局提供了一些方便,但是也增加了布局系統(tǒng)的復雜性罩驻。
因為在 iOS 中幾乎所有的視圖都繼承自 UIView
穗酥,這樣也同時繼承了 frame
屬性,在使用 Auto Layout 和 UIStackView
時惠遏,并沒有禁用 frame
布局砾跃,所以在混用卻沒有掌握技巧時可能會有一些比較奇怪的問題。
其實节吮,在混用 Auto Layout 和 frame
時遇到的大部分奇怪的問題都是因為 translatesAutoresizingMaskIntoConstraints 屬性沒有被正確設置的原因抽高。
If this property’s value is true, the system creates a set of constraints that duplicate the behavior specified by the view’s autoresizing mask. This also lets you modify the view’s size and location using the view’s frame, bounds, or center properties, allowing you to create a static, frame-based layout within Auto Layout.
在這里就不詳細解釋該屬性的作用和使用方法了。
對動畫的影響
在 Auto Layout 出現之前透绩,由于一切布局都是使用 frame
工作的翘骂,所以在 iOS 中完成對動畫的編寫十分容易。
UIView.animate(withDuration: 1.0) {
view.frame = CGRect(x: 10, y: 10, width: 200, height: 200)
}
而當大部分的 iOS 應用都轉而使用 Auto Layout 之后帚豪,對于視圖大小碳竟、位置有關的動畫就比較麻煩了:
topConstraint.constant = 10
leftConstraint.constant = 10
heightConstraint.constant = 200
widthConstraint.constant = 200
UIView.animate(withDuration: 1.0) {
view.layoutIfNeeded()
}
我們需要對視圖上的約束對象一一修改并在最后調用 layoutIfNeeded
方法才可以完成相同的動畫。由于 Auto Layout 對動畫的支持并不是特別的優(yōu)秀狸臣,所以在很多時候筆者在使用 Auto Layout 的視圖上莹桅,都會使用 transform
屬性來改變視圖的位置,這樣雖然也沒有那么的優(yōu)雅烛亦,不過也是一個比較方便的解決方案统翩。
frame 的問題
每一個 UIView
的 frame
屬性其實都是一個 CGRect
結構體,這個結構體展開之后有四個組成部分:
- origin
- x
- y
- size
- width
- height
當我們設置一個 UIView
對象的 frame
屬性時此洲,其實是同時設置了它在父視圖中的位置和它的大小厂汗,從這里可以獲得一條比較重要的信息:
iOS 中所有的
UIView
對象都是使用frame
布局的,否則frame
中的origin
部分就失去了意義呜师。
但是如果為 UIStackView
中的視圖設置 frame
的話娶桦,這個屬性就完全沒什么作用了,比如下面的代碼:
UIStackView *stackView = [[UIStackView alloc] init];
stackView.frame = self.view.frame;
[self.view addSubview:stackView];
UIView *greenView = [[UIView alloc] init];
greenView.backgroundColor = [UIColor greenColor];
greenView.frame = CGRectMake(0, 0, 100, 100);
[stackView addArrangedSubview:greenView];
UIView *redView = [[UIView alloc] init];
redView.backgroundColor = [UIColor redColor];
redView.frame = CGRectMake(0, 0, 100, 100);
[stackView addArrangedSubview:redView];
frame
屬性在 UIStackView
上基本上就完全失效了汁汗,我們還需要使用約束來控制 UIStackView
中視圖的大小衷畦,不過如果你要使用 frame
屬性來查看視圖在父視圖的位置和大小,在恰當的時機下是可行的知牌。
談談 origin
但是 frame
的不正確使用會導致視圖之間的耦合祈争,如果內部視圖設置了自己在父視圖中的 origin
,但是父視圖其實并不會使用直接 frame
布局該怎么辦角寸?比如菩混,父視圖是一個 UIStackView
忿墅,它就會重寫子視圖的 origin
甚至是沒有正確設置的 size
屬性。
最重要的是 UIView
上 frame
的設計導致了視圖之間可能會有較強的耦合沮峡,因為子視圖不應該知道自己在父視圖中的位置疚脐,它應該只關心自己的大小。
也就是作為一個簡單的 UIView
它應該只能設置自己的 size
而不是 origin
邢疙,因為父視圖可能是一個 UIStackView
也可能是一個 UITableView
甚至是一個扇形的視圖也不是不可能棍弄,所以位置這一信息并不是子視圖應該關心的。
如果視圖設置了自己的 origin
其實也就默認了自己的父視圖一定是使用 frame
進行布局的疟游,而一旦依賴于外部的信息呼畸,它就很難進行復用了。
再談 size
關于視圖大小的確認颁虐,其實也是有一些問題的蛮原,因為視圖在布局時確實可能依賴于父視圖的大小,或者更確切的說是需要父視圖提供一個可供布局的大小聪廉,然后讓子視圖通過這個 CGSize
返回一個自己需要的大小給父視圖。
這種計算視圖大小的方式故慈,其實比較像 Texture 也就是原來的 AsyncDisplayKit 中對于布局系統(tǒng)的實現板熊。
父視圖通過調用子視圖的 -layoutSpecThatFits:
方法獲取子視圖布局所需要的大小,而子視圖通過父視圖傳入的 CGSizeRange
來設置自己的大小察绷。
- (ASLayoutSpec *)layoutSpecThatFits:(ASSizeRange)constrainedSize
...
}
通過這種方式干签,子視圖對父視圖一無所知,它不知道父視圖的任何屬性拆撼,只通過 -layoutSpecThatFits:
方法傳入的參數進行布局容劳,實現了解耦以及代碼復用。
storyboard 和 xib
小結
由于確實需要對多尺寸的屏幕進行適配闸度,蘋果推出 Auto Layout 和 UIStackView
的初衷也沒有錯竭贩,但是在筆者看來,因為絕大部分視圖都繼承自 UIView
莺禁,所以在很多情況下并沒有對開發(fā)者進行強限制留量,比如在使用 UIStackView
時只能使用 flexbox 式的布局,在使用 Auto Layout 時也只能使用約束對視圖進行布局等等哟冬,所以在很多時候會帶來一些不必要的問題楼熄。
同時 UIView
中的 frame
屬性雖然在一開始能夠很好的解決的布局的問題,但是隨著布局系統(tǒng)變得越來越復雜浩峡,使得很多 ?UI 組件在與非 frame
布局的容器同時使用時產生了沖突可岂,最終破壞了良好的封裝性。
到目前為止 iOS 中的視圖層的問題主要就是 UIView
作為視圖層中的上帝類翰灾,提供的 frame
布局系統(tǒng)不能良好的和其他布局系統(tǒng)工作缕粹,在一些時候 frame
屬性完全成為了擺設稚茅。
其他平臺對視圖層的設計
在接下來的文章中,我們會介紹和分析其他平臺 Android致开、Web 前端以及后端是如何對視圖層進行設計的峰锁。
Android 與 View
與 iOS 上使用命令式的風格生成界面不同,Android 使用聲明式的 XML 對界面進行描述双戳,在這里舉一個最簡單的例子:
<android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context="com.example.draveness.myapplication.DisplayMessageActivity">
<TextView
android:id="@+id/textView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:text="TextView"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</android.support.constraint.ConstraintLayout>
整個 XML 文件同時描述了視圖的結構和樣式虹蒋,而這也是 Android 對于視圖層的設計方式,將結構和樣式混合在一個文件中飒货。
我們首先來分析一下上述代碼的結構魄衅,整個 XML 文件中只有兩個元素,如果我們去掉其中所有的屬性塘辅,整個界面的元素就是這樣的:
<ConstraintLayout>
<TextView/>
</ConstraintLayout>
由一個 ConstraintLayout
節(jié)點包含一個 TextView
節(jié)點晃虫。
View 和 ViewGroup
我們再來看一個 Android 中稍微復雜的視圖結構:
<LinearLayout>
<RelativeLayout>
<ImageView/>
<LinearLayout>
<TextView/>
<TextView/>
</LinearLayout>
</RelativeLayout>
<View/>
</LinearLayout>
上面的 XML 代碼描述了一個更加復雜的視圖樹,這里通過一張圖更清晰地展示該視圖表示的結構:
我們可以發(fā)現扣墩,Android 的視圖其實分為兩類:
- 一類是不能有子節(jié)點的視圖哲银,比如
View
、ImageView
和TextView
等呻惕; - 另一類是可以有子節(jié)點的視圖荆责,比如
LinearLayout
和RelativeLayout
等;
在 Android 中亚脆,這兩類的前者都是 View
的子類做院,也就是視圖;后者是 ViewGroup
的子類濒持,它主要充當視圖的容器键耕,與它的子節(jié)點以樹形的結構形成了一個層次結構。
這種分離視圖和容器的方式很好的分離了職責柑营,將管理和控制子視圖的功能劃分給了 ViewGroup
屈雄,將顯示內容的職責拋給了 View
對各個功能進行了合理的拆分。
子視圖的布局屬性只有在父視圖為特定 ViewGroup
時才會激活官套,否則就會忽略在 XML 中聲明的屬性棚亩。
混合的結構與樣式
在使用 XML 或者類 XML 的這種文本來描述視圖層的內容時,總會遇到一種無法避免的爭論:樣式到底應該放在哪里虏杰?上面的例子顯然說明了 Android 對于這一問題的選擇讥蟆,也就是將樣式放在 XML 結構中。
這一章節(jié)中并不會討論樣式到底應該放在哪里這一問題纺阔,我們會在后面的章節(jié)中具體討論瘸彤,將樣式放在 XML 結構中和單獨使用各自的優(yōu)缺點。
Web 前端
隨著 Web 前端應用變得越來越復雜笛钝,在目前的大多數 Web 前端項目的實踐中质况,我們已經會使用前后端分離方式開發(fā) Web 應用愕宋,而 Web 前端也同時包含 Model、View 以及 Controller 三部分结榄,不再通過服務端直接生成前端的 HTML 代碼了中贝。
現在最流行的 Web 前端框架有三個,分別是 React臼朗、Vue 和 Angular邻寿。不過,這篇文章會以最根本的 HTML 和 CSS 為例视哑,簡單介紹 Web 前端中的視圖層是如何工作的绣否。
<div>
<h1 class="text-center">Header</h1>
</div>
.text-center {
text-align: center;
}
在 HTML 中其實并沒有視圖和容器這種概念的劃分,絕大多數的元素節(jié)點都可以包含子節(jié)點挡毅,只有少數的無內容標簽蒜撮,比如說 br
、hr
跪呈、img
段磨、input
、link
以及 meta
才不會解析自己的子節(jié)點耗绿。
分離的結構與樣式
與 Android 在定義視圖時苹支,使用混合的結構與樣式不同,Web 前端在視圖層中缭乘,采用 HTML 與 CSS 分離沐序,即結構與樣式分離的方式進行設計琉用;雖然在 HTML 中堕绩,我們也可以使用 style
將 CSS 代碼寫在視圖層的結構中,不過在一般情況下邑时,我們并不會這么做奴紧。
<body style="background-color:powderblue;">
</body>
結構與樣式
在這一章節(jié)中,我們會對結構與樣式組織方式之間的優(yōu)劣進行簡單的討論晶丘。
Android 和 Web 前端使用不同的方式對視圖層的結構和樣式進行組織黍氮,前者使用混合的方式,后者使用分離的結構和樣式浅浮。
相比于分離的組織方式沫浆,混合的組織方式有以下的幾個優(yōu)點:
- 不需要實現元素選擇器,降低視圖層解析器實現的復雜性滚秩;
- 元素的樣式是內聯(lián)的专执,對于元素的樣式的定義一目了然,不需要考慮樣式的繼承等復雜特性郁油;
分離的組織方式卻正相反:
- 元素選擇器的實現本股,增加了 CSS 樣式代碼的復用性攀痊,不需要多次定義相同的樣式;
- 將 CSS 代碼從結構中抽離能夠增強 HTML 的可讀性拄显,可以非常清晰苟径、直觀的了解 HTML 的層級結構;
對于結構與樣式躬审,不同的組織方式能夠帶來不同的收益棘街,這也是在設計視圖層時需要考慮的事情,我們沒有辦法在使用一種組織方式時獲得兩種方式的優(yōu)點盒件,只能盡可能權衡利弊蹬碧,選擇最合適的方法。
后端的視圖層
這一章節(jié)將會研究一下后端視圖層的設計炒刁,不過在真正開始分析其視圖層設計之前恩沽,我們需要考慮一個問題,后端的視圖層到底是什么翔始?它有客戶端或者 Web 前端中的用于展示內容視圖層么罗心?
這其實是一個比較難以回答的問題,不過嚴格意義上的后端是沒有用于展示內容的視圖層的城瞎,也就是為客戶端提供 API 接口的后端渤闷,它們的視圖層,其實就是用于返回 JSON 的模板脖镀。
json.extract! user, :id, :mobile, :nickname, :gender, :created_at, :updated_at
json.url user_url user, format: :json
在 Ruby on Rails 中一般都是類似于上面的 jbuilder 代碼飒箭。擁有視圖層的后端應用大多都是使用了模板引擎技術,直接為 HTTP 請求返回渲染之后的 HTML 和 CSS 等前端代碼蜒灰。
總而言是弦蹂,使用了模板引擎的后端應用其實是混合了 Web 前端和后端,整個服務的視圖層其實就是 Web 前端的代碼强窖;而現在的大多數 Web 應用凸椿,由于遵循了前后端分離的設計,兩者之間的通信都使用約定好的 API 接口翅溺,所以后端的視圖層其實就是單純的用于渲染 JSON 的代碼脑漫,比如 Rails 中的 jbuilder。
理想中的視圖層
iOS 中理想的視圖層需要解決兩個最關鍵的問題:
- 細分
UIView
的職責咙崎,將其分為視圖和容器兩類优幸,前者負責展示內容,后者負責對子視圖進行布局褪猛; - 去除整個視圖層對于
frame
屬性的依賴网杆,不對外提供frame
接口,每個視圖只能知道自己的大小跛璧;
解決上述兩個問題的辦法就是封裝原有的 UIView
類严里,使用組合模式為外界提供合適的接口。
細分 UIView 的職責
Node
會作為 UIView
的代理追城,同時也作為整個視圖層新的根類刹碾,它將屏蔽掉外界與 UIView
層級操作的有關方法,比如說:-addSubview:
等座柱,同時迷帜,它也會屏蔽掉 frame
屬性,這樣每一個 Node
類的實例就只能設置自己的大小了色洞。
public class Node: Buildable {
public typealias Element = Node
public let view: UIView = UIView()
@discardableResult
public func size(_ size: CGSize) -> Element {
view.size = size
return self
}
}
上面的代碼簡單說明了這一設計的實現原理戏锹,我們可以理解為 Node
作為 UIView
的透明代理,它不提供任何與視圖層級相關的方法以及 frame
屬性火诸。
容器的實現
除了添加一個用于展示內容的 Node
類锦针,我們還需要一個 Container
的概念,提供為管理子視圖的 API 和方法置蜀,在這里奈搜,我們添加了一個空的 Container
協(xié)議:
public protocol Container { }
利用這個協(xié)議,我們構建一個 iOS 中最簡單的容器 AbsoluteContainer
盯荤,內部使用 frame
對子視圖進行布局馋吗,它應該為外界提供添加子視圖的接口,在這里就是 build(closure:)
方法:
public class AbsoluteContainer: Node, Container {
typealias Element = AbsoluteContainer
@discardableResult
public func build(closure: () -> Node) -> Relation<AbsoluteContainer> {
let node = closure()
view.addSubview(node.view)
return Relation<AbsoluteContainer>(container: self, node: node)
}
}
該方法會在調用后返回一個 Relation
對象秋秤,這主要是因為在這種設計下的 origin
或者 center
等屬性不再是 Node
的一個接口宏粤,它應該是 Node
節(jié)點出現在 AbsoluteContainer
時的產物,也就是說灼卢,只有在這兩者同時出現時绍哎,才可以使用這些屬性更新 Node
節(jié)點的位置:
public class Relation<Container> {
public let container: Container
public let node: Node
public init(container: Container, node: Node) {
self.container = container
self.node = node
}
}
public extension Relation where Container == AbsoluteContainer {
@discardableResult
public func origin(_ origin: CGPoint) -> Relation {
node.view.origin = origin
return self
}
}
這樣就完成了對于 UIView
中視圖層級和位置功能的剝離,同時使用透明代理以及 Relation
為 Node
提供其他用于設置視圖位置的接口芥玉。
這一章節(jié)中的代碼都來自于 Mineral蛇摸,如果對代碼有興趣的讀者备图,可以下載自行查看灿巧。
總結
Cocoa Touch 中的 UIKit 對視圖層的設計在一開始確實是沒有問題的,主要原因是在 iOS 早期的布局方式并不復雜揽涮,只有單一的 frame
布局抠藕,而這種方式也恰好能夠滿足整個平臺對于 iOS 應用開發(fā)的需要,但是隨著屏幕尺寸的增多蒋困,蘋果逐漸引入的其它布局方式與原有的體系發(fā)生了一些沖突盾似,導致在開發(fā)時可能遇到奇怪的問題,而這也是本文想要解決的,將原有屬于 UIView
的職責抽離出來零院,提供更合理的抽象溉跃。