前言
本篇所涉及的性能問(wèn)題我都將根據(jù)滑動(dòng)的流暢性來(lái)評(píng)判, 包括掉幀情況和一些實(shí)際體驗(yàn)
ASDK 已經(jīng)改名為 Texture, 我習(xí)慣稱作 ASDK
編譯環(huán)境: MacOS 10.13.3, Xcode 9.2
參與測(cè)試機(jī)型: iPhone 6 10.3.3, iPhone 7 11.2.1, iPhone X 11.2.5, 默認(rèn) iPhone 6
TableView / TableNode 包含的 TableViewCell / CellNode: 默認(rèn)復(fù)雜程度一般, 包含 1~2 張圖片和 2~4 條文本展示, 圖片有圓角
列表滑動(dòng)卡頓的原因及優(yōu)化
大牛們把原因都說(shuō)的很清楚了, 導(dǎo)致的結(jié)果就是 16ms 不足以渲染一幀, 產(chǎn)生掉幀卡頓
下面是嘗試過(guò)的一些優(yōu)化:
圓角
-
使用一張圓角圖片覆蓋, 經(jīng)典文章 Corner Rounding, HYBImageCliped 也是這么做的
image 異步裁剪圖片: 通過(guò) UIGraphics 對(duì)圖片進(jìn)行裁剪, 可能造成內(nèi)存暴漲
行高緩存
老生長(zhǎng)談了, 除 UITableView-FDTemplateLayoutCell 之外, QMUI 中也有提供一套緩存行高方案
數(shù)據(jù)預(yù)加工
具體是在 JSON 轉(zhuǎn) Model 后把文本轉(zhuǎn)為富文本, 處理一些弱邏輯等, 之后賦值就可以直接展示了
咳咳, 這個(gè)感覺(jué)不到什么效果
圖形預(yù)加工
例如處理圖片遮罩或固定的圖標(biāo), 一般是直接使用多層視圖實(shí)現(xiàn)
我曾嘗試把三張小圖繪制到一張大圖上再進(jìn)行展示, 于是乎, 異步復(fù)用問(wèn)題除外, 內(nèi)存炸了, 最終還是老老實(shí)實(shí)用多個(gè)視圖實(shí)現(xiàn)
為什么要使用 ASDK
圖形異步渲染
通常我們認(rèn)為 UIKit 是不能渲染于非主線程的, 一旦你這么做, 就可能會(huì)導(dǎo)致崩潰, 無(wú)法正常顯示等問(wèn)題, 而 ASDK 為什么可以呢, 因?yàn)?ASDisplayNode 是線程安全的, Node 創(chuàng)建時(shí), 不會(huì)立即在其內(nèi)部新建 UIView 和 CALayer, 直到主線程第一次訪問(wèn)時(shí)才會(huì)生成對(duì)應(yīng)的對(duì)象, 除此之外, 還通過(guò)圖層預(yù)合成和基于 Runloop 的異步并發(fā), 使其擁有更好的性能 ASAsyncTransactionGroup
這個(gè)特點(diǎn)帶來(lái)的相關(guān)實(shí)際體驗(yàn)就是: 安心的進(jìn)行異步繪圖, 如圓角裁剪, 增加遮罩等, 這在 UIKit 中是足以毀滅人生的, 內(nèi)存暴漲, 異步復(fù)用, 性能極差
不過(guò)低性能設(shè)備下還是會(huì)出現(xiàn)明顯空白
<video src=”https://img.didee.cn/video/ASDKDemo-Video.mp4" poster=https://img.didee.cn/video/post/ASDKDemo-Video.png!jpgwidth=100% controls=”controls” preload=”none” >Video
預(yù)加載數(shù)據(jù)和對(duì)象
首先來(lái)一張 Gif 體驗(yàn)一下, 實(shí)際上使用 ASDK 開(kāi)發(fā)完成后對(duì)比也是如此, 有種網(wǎng)速變快了的錯(cuò)覺(jué)
ASDK 中的 ASRangeController, ASTableView, ASCollectionView 相對(duì)于 UIKit 原生控件的特點(diǎn)是可用于監(jiān)控視圖的可見(jiàn)區(qū)域, 維護(hù)工作區(qū)域, 在合適的時(shí)機(jī)觸發(fā)網(wǎng)絡(luò)請(qǐng)求以及繪制, 單元格的異步布局
[圖片上傳失敗...(image-29e19e-1536029030806)]
這里推薦閱讀: 預(yù)加載與智能預(yù)加載(iOS)
異于原生控件的復(fù)用機(jī)制
單一的 Cell
意思是某個(gè) List 展示的樣式只有一種, TableView 只需要注冊(cè)一個(gè) Cell
這種情況下, 如果常規(guī)的一些優(yōu)化得當(dāng), 滾動(dòng)的流暢性還是可以接受的(與 ASDK 差距微小, 但仍然肉眼可分辨)
此時(shí)的差距主要體現(xiàn)在列表某項(xiàng)數(shù)據(jù)第一次展示, 以及 TableView 在分頁(yè)加載時(shí)產(chǎn)生的等待較長(zhǎng), 當(dāng)然, 這兩點(diǎn)也是可以繼續(xù)優(yōu)化和解決的
相反的, 也就是來(lái)回滑動(dòng)已經(jīng)展示過(guò)的數(shù)據(jù), 兩者的差距就非常小了, 大概是 59.7 - 59.9 和 59.9 的區(qū)別 (我瞎扯的)
綜上, 優(yōu)化得當(dāng)?shù)那闆r下, 單一的 Cell 情況下 UIKit 與 ASDK 的差距不明顯
<video src=”https://img.didee.cn/video/NiceFilm-Home.mp4" poster=https://img.didee.cn/video/post/NiceFilm-Home.png!jpg width=100% controls=”controls” preload=”none” >Video
多種 Cell
表示某 List 中有多種不同的樣式, TableView 必須要通過(guò)注冊(cè) N 個(gè) Cell 來(lái)實(shí)現(xiàn)
這種情況下, 假設(shè)有 5 種 Cell, 屏幕可同時(shí)展示 6 條 Cell, 此時(shí)若第一屏幕剛好展示的就包含全部 5 種 Cell , 那么后續(xù)的滑動(dòng)情況將與單一的 Cell表現(xiàn)一致, 若第一屏幕展示的內(nèi)容只包含一種, 其他 4 種沒(méi)有在屏幕上出現(xiàn)過(guò), 那么當(dāng)某一種首次出現(xiàn)在屏幕上時(shí), 便會(huì)出現(xiàn)明顯的卡頓; 我嘗試過(guò)解決這個(gè)問(wèn)題, 提前創(chuàng)建所有的 Cell 實(shí)例對(duì)象, 緩存和復(fù)用相同的子視圖, 異步預(yù)繪制為一張圖片并緩存(坑), 都收效漸微
因 ASDK 支持預(yù)渲染, 與處理單種 Cell 沒(méi)有區(qū)別, 依舊 59.9
復(fù)用的差別
TableView 的復(fù)用機(jī)制我是既愛(ài)又恨的, 方便之處在于直接與數(shù)據(jù)綁定后, 可以方便的更新和修改, 只需保證 setModel 簡(jiǎn)潔就 OK, 只是當(dāng)業(yè)務(wù)綁定較多時(shí)就比較麻煩了
下面重點(diǎn)說(shuō)說(shuō) TableNode, TableNode 的復(fù)用機(jī)制就是沒(méi)有復(fù)用, 只有緩存, 每個(gè) CellNode 都是全新的, 因此會(huì)有一些特殊的地方:
CellNode 與數(shù)據(jù)源沒(méi)有綁定關(guān)系: 創(chuàng)建后就算把數(shù)據(jù)源刪除, TableNode 依然可以正常展示
數(shù)據(jù)直接決定要?jiǎng)?chuàng)建一個(gè)怎樣的 CellNode: 這一點(diǎn)很重要, TableViewCell 的展示大致為: 添加空假數(shù)據(jù)子視圖 -> 數(shù)據(jù)填充 -> 刷新, 涉及布局或圖文時(shí)會(huì)更復(fù)雜
CellNode 只有一步: 添加真數(shù)據(jù)的子視圖; 因此可以直接根據(jù)業(yè)務(wù)邏輯對(duì)控件和布局做出處理, 而不用一次或多次刷新
Demo: 此處需求為每組一個(gè)大圖 + N個(gè)小圖, 每組 3 或 5 個(gè)
解決思路: TableView 的方式是創(chuàng)建 5 個(gè), 根據(jù)數(shù)量顯隱下面兩個(gè), 或者兩種 Cell, 把 3 和 5 的情況分別對(duì)應(yīng), 除此之外, 最重要的是: 祈禱數(shù)據(jù)正常, 每組數(shù)據(jù)個(gè)數(shù)僅為 3 或 5
此時(shí)若使用 TableNode 就靈活多了, 可以根據(jù)需要(數(shù)據(jù)個(gè)數(shù)), 加入需要的子視圖, 我的思路是把頂部的大圖固定, 剩下的兩個(gè)為一行進(jìn)行添加, 就算總數(shù)為偶數(shù)也是沒(méi)有任何額外消耗的, 具體參見(jiàn) ASDKDemo
<video src=”https://img.didee.cn/video/ASDKDemo-Muster.mp4" poster=https://img.didee.cn/video/post/ASDKDemo-Muster.png!jpgwidth=100% controls=”controls” preload=”none” >Video
Flex 布局
值得學(xué)習(xí)的理由
ASDK 使用的是 Flex 布局, 且面向?qū)ο?/p>
偷一張圖
具體對(duì)比: iOS 上的 FlexBox 布局
簡(jiǎn)單來(lái)說(shuō), 缺點(diǎn)只有一個(gè), 就是學(xué)習(xí)曲線相對(duì) Frame AutoLayout 更陡峭, 而優(yōu)點(diǎn)是 性能與 Frame 相當(dāng), 上手后比 AutoLayout 還簡(jiǎn)單, 如果你已經(jīng)開(kāi)始嘗試, 請(qǐng)堅(jiān)持下去
不同的方式和思想
AutoLayout
使用 AutoLayout 時(shí)我心里想的無(wú)外乎:
- 我要把你放在左上角: Left Top
- 把你放在它右邊: LeftTo(它)
- 放中間: Center
- 至少/至多離它多遠(yuǎn): less / greater
缺點(diǎn)是視圖之間的依賴性太強(qiáng), 可讀性維護(hù)性較差(更差的是 Frame), 例如排列數(shù)個(gè)距離不等控件, 就會(huì)很厭煩, 然后 cv 重復(fù)代碼; 處理多個(gè)多行文本垂直排列時(shí)很惡心, 想要處理好最終需要去計(jì)算文字行高, 外加入自定行間距; …
Flex
例如 Demo 中的
我做的事情是:
- 聲明大圖的比例: Ratio(9.0/16.0); 那么這個(gè)聲明存儲(chǔ)為
postImageRatioSpec
- 聲明大標(biāo)題的內(nèi)邊距: Inset(8, 8, 8, 8);
titleInsetSpec
- 聲明
titleInsetSpec
的位置是垂直方向下的最尾部;titleRelativeSpec
- 聲明
titleRelativeSpec
是覆蓋到postImageRatioSpec
上:titleOverlaySpec
此時(shí)大圖和文字布局完成
接下來(lái)是用戶欄:
- 聲明用戶頭像和名稱水平排列, 水平方向從左也就是從頭部開(kāi)始, 距離 4, 對(duì)齊方式為居中, 此時(shí)的居中為垂直方向;
leftStackSpec
- 同理, 聲明兩個(gè)圖標(biāo)為水平, 尾部起始, 距離4, 居中;
rightStackSpec
- 接下來(lái), 聲明
leftStackSpec
和rightStackSpec
水平排列, 等間距排列填充(實(shí)際是為 left 和 rightStack 進(jìn)行填充), 距離 8, 對(duì)齊方式填充(無(wú)實(shí)際作用, 由于子視圖同為 Stack 且都是水平方向);userStackSpec
最后:
- 聲明
titleOverlaySpec
和userStackSpec
垂直排列, 自上而下, 對(duì)齊方式填充(同理于userStackSpec
, 此處影響的是userStackSpec
);videoStackSpec
- 聲明
videoStackSpec
的內(nèi)邊距: Inset(16, 16, 0, 16);videoInsetSpec
特別注意 userStackSpec
和 videoStackSpec
, StackSpec 多層疊加后, 父子間是存在影響的, 我在使用中也感覺(jué)比較奇怪, 具體需要自行嘗試體會(huì)..
具體實(shí)現(xiàn)代碼: VideoCellNode.m , UserNode.m
相關(guān)鏈接 (不分先后)
文章
AsyncDisplayKit 系列教程 —— 為什么要使用 AsyncDisplayKit
Via: Conver
使用替換UIImageView -> ASImageNode/ASNetworkImageNode
APP性能的優(yōu)化,一直都是任重而道遠(yuǎn)凭豪,對(duì)于如今需要承載更多信息的APP來(lái)說(shuō)更是突出焙蹭,值得慶幸的蘋(píng)果在這方面做得至少比安卓讓開(kāi)發(fā)者省心嫂伞。UIKit 控件雖然在大多數(shù)情況下都能滿足用戶對(duì)于流暢性的需求孔厉,但有時(shí)候還是難以達(dá)到理想效果。
AsyncDisplayKit(以下簡(jiǎn)稱ASDK) 的出現(xiàn)至少又給了開(kāi)發(fā)者一個(gè)不錯(cuò)的選擇末早。畢竟Paper(雖然 Facebook 已經(jīng)關(guān)閉了這個(gè)應(yīng)用)當(dāng)年有著炫酷的效果的同時(shí)依然保持較好的流暢性也得益于 ASDK 的加入烟馅。在Paper發(fā)布的幾個(gè)月后 Facebook 就干脆從中剝離出來(lái)成為一個(gè)獨(dú)立的庫(kù),就在前兩天 ASDK 剛好發(fā)布了 2.0 版本然磷。
目前據(jù)我所知國(guó)內(nèi)比較知名有 輕芒閱讀(豌豆莢一覽) 、 即刻 和 Yep 在用ASDK姿搜。 拿 即刻 來(lái)說(shuō)包括 消息盒子
寡润、主題的詳情頁(yè)
、動(dòng)態(tài)通知
梭纹、我的喜歡
、評(píng)論頁(yè)
致份、最近熱門(mén)
变抽、即刻小報(bào)
氮块、他關(guān)注的人
绍载、關(guān)注他的人
以及搜索頁(yè)
都用到了 ADSK。
目前 AsyncDisplayKit 已經(jīng)從 facebook 遷移至 TextureGroup 新的項(xiàng)目地址是 Texture
控件
Texture 幾乎涵蓋了常用的控件滔蝉,下面是 Texture
和 UIKit
的對(duì)應(yīng)關(guān)系,有些封裝可以說(shuō)非常良心蝠引。
Nodes:
Texture | UIKit |
---|---|
ASDisplayNode | UIView |
ASCellNode | UITableViewCell/UICollectionViewCell |
ASTextNode | UILabel |
ASImageNode | UIImageView |
ASNetworkImageNode | UIImageView |
ASVideoNode | AVPlayerLayer |
ASControlNode | UIControl |
ASScrollNode | UIScrollView |
ASControlNode | UIControl |
ASEditableTextNode | UITextView |
ASMultiplexImageNode | UIImageView |
Node Containers
Texture | UIKit |
---|---|
ASViewController | UIViewController |
ASTableNode | UITableView |
ASCollectionNode | UICollectionView |
ASPagerNode | UICollectionView |
子父類關(guān)系:
- ASDisplayNode
- ASCellNode
- ASTextCellNode
- ASCollectionNode
- ASPagerNode
- ASControlNode
- ASButtonNode
- ASImageNode
- ASMapNode
- ASMultiplexImageNode
- ASNetworkImageNode
- ASVideoNode
- ASTextNode
- ASTextNode2
- ASEditableTextNode
- ASScrollNode
- ASTableNode
- ASVideoPlayerNode
- ASCellNode
ASDisplayNode:
作用同等于UIView
阳谍,是所有 Node 的父類,需要注意的是 ASDisplayNode
其實(shí)擁有一個(gè)view
屬性矫夯,所以ASDisplayNode
及其子類都可以通過(guò)這個(gè)view
來(lái)添加UIKit
控件,這樣一來(lái) Texture
和 UIKit
混用是完全沒(méi)問(wèn)題的吊洼。
ASDisplayNode
中添加 UIKit
UIView *otherView = [[UIView alloc] init];
otherView.frame = ...;
[self.view addSubview:otherView];
或
ASDisplayNode *gradientNode = [[ASDisplayNode alloc] initWithViewBlock:^UIView * _Nonnull{
UIView *view = [[UIView alloc] init];
return view;
}];
第二種的初始化最終生成的就是 block 返回的 UIKit
對(duì)象训貌,但外部表現(xiàn)出來(lái)的是 ASDisplayNode
。這樣子的好處在于布局融蹂,關(guān)于布局旺订,后面會(huì)講到超燃。
UIKit
中添加 ASDisplayNode
ASImageNode *imageNode = [[ASImageNode alloc] init];
imageNode.image = [UIImage imageNamed:@"iconShowMore"];
imageNode.frame = ...;
[self addSubnode:imageNode];
self.imageNode = imageNode;
ASCellNode:
作用同等于 UITableViewCell
或 UICollectionViewCell
区拳,自帶 indexPath
屬性,有些時(shí)候很有用意乓。
ASTextNode
作用同等于UILabel
,和 UILabel
不同的是 ASTextNode
必須通過(guò) attributedText
添加文字届良。
ASTextNode2
在 ASTextNode 基礎(chǔ)修復(fù)了一些 Bug
ASImageNode
作用同等于 UIImageView
笆凌,但是只能設(shè)置靜態(tài)圖片,如果需要使用網(wǎng)絡(luò)圖片乞而,請(qǐng)使用 ASNetworkImageNode
。
ASNetworkImageNode
作用同等于 UIImageView
慢显,如果使用網(wǎng)絡(luò)圖片請(qǐng)使用此類爪模,Texture
用的是第三方的圖片加載庫(kù)PINRemoteImage荚藻,ASNetworkImageNode
其實(shí)并不支持 gif屋灌,如果需要顯示 gif 推薦使用FLAnimatedImage。
ASButtonNode
作用同等于 UIButton
应狱,需要注意的是下面這個(gè)兩個(gè)屬性
@property (nonatomic, assign) CGFloat contentSpacing;// 設(shè)置圖片和文字的間距
@property (nonatomic, assign) ASButtonNodeImageAlignment imageAlignment;// 圖片和文字的排列方式,
簡(jiǎn)直要抱頭痛哭一下??疾呻,imageAlignment
可以設(shè)置兩個(gè)值:
ASButtonNodeImageAlignmentBeginning, // 圖片在前除嘹,文字在后
ASButtonNodeImageAlignmentEnd// 文字在前,圖片在后
ASTableNode
作用同等于 UITableView
罐韩,但是實(shí)現(xiàn)上并沒(méi)有采用 UITableView
的重用機(jī)制,而是通過(guò)用戶滾動(dòng)對(duì)需要顯示的視圖進(jìn)行add
和 不需要的進(jìn)行remove
的操作(我猜的)散吵。另外重要的一點(diǎn):ASTableNode
并沒(méi)有像 UITableView
一樣提供一個(gè)-tableView:heightForRowAtIndexPath:
協(xié)議方法來(lái)決定每個(gè) Cell 的高度龙考,而是由 ASCellNode
本身決定。這樣帶來(lái)的另外一個(gè)好處是矾睦,動(dòng)態(tài)高度的實(shí)現(xiàn)可謂是易如反掌,具體可以看官方 Demo 中的 Kittens枚冗。
如何正確的使用
對(duì)于現(xiàn)有的項(xiàng)目中出現(xiàn)的并不嚴(yán)重的性能問(wèn)題缓溅,我的建議是用對(duì)應(yīng)的 Texture 控件代替即可。
比如把 UIImageView -> ASImageNode/ASNetworkImageNode
坛怪,UILabel -> ASTextNode
之類的淤齐,而不是把原有的 UITableView -> ASTableNode
,UICollectionView -> ASCollectionNode
袜匿。
在 Cell 中替換 UIImageView
ASImageNode *imageNode = [[ASImageNode alloc] init];
imageNode.image = [UIImage imageNamed:@"iconShowMore"];
imageNode.frame = ...;
[self.contentView addSubnode:imageNode];
self.imageNode = imageNode;
原因有以下幾點(diǎn):
-
ASCellNode
內(nèi)部的布局會(huì)用到 Texture 本身有一套布局方案,然而這套布局學(xué)習(xí)成本略高居灯。 - 包括
ASTableNode
和ASCollectionNode
和原生的UITableView
和UICollectionView
有較大的 API 改變祭务,侵略性較大,不太利于后期維護(hù)怪嫌。 - 第三方的支持問(wèn)題,例如 DZNEmptyDataSet 對(duì)于
ASTableNode
和ASCollectionNode
的支持還是有點(diǎn)問(wèn)題岩灭。
所以當(dāng)你還沒(méi)有做好應(yīng)付上面三個(gè)問(wèn)題的準(zhǔn)備拌倍,簡(jiǎn)單的 UIKit -> Texture
替換才是正確選擇。
布局
閱讀 Texture 布局篇
其他
刷新列表
無(wú)論是 ASTableNode
還是 ASCollectionNode
當(dāng)列表中已經(jīng)有數(shù)據(jù)顯示了噪径,調(diào)用 reloadData
你會(huì)發(fā)現(xiàn)列表會(huì)閃一下贰拿。最常見(jiàn)的案例是上拉加載更多獲取到新數(shù)據(jù)后調(diào)用 reloadData
刷新列表用戶體驗(yàn)會(huì)比較差熄云,事實(shí)上官方文檔在 [Batch Fetching API] 給出了解決辦法:
- (void)tableNode:(ASTableNode *)tableNode willBeginBatchFetchWithContext:(ASBatchContext *)context
{
// Fetch data most of the time asynchronoulsy from an API or local database
NSArray *newPhotos = [SomeSource getNewPhotos];
// Insert data into table or collection node
[self insertNewRowsInTableNode:newPhotos];
// Decide if it's still necessary to trigger more batch fetches in the future
_stillDataToFetch = ...;
// Properly finish the batch fetch
[context completeBatchFetching:YES];
}
獲取新數(shù)據(jù)后直接插入到列表中膨更,而不是刷新整個(gè)列表,比如:
- (void)insertSections:(NSIndexSet *)sections withRowAnimation:(UITableViewRowAnimation)animation;
和
- (void)insertRowsAtIndexPaths:(NSArray<NSIndexPath *> *)indexPaths withRowAnimation:(UITableViewRowAnimation)animation;
加載數(shù)據(jù)
細(xì)心的同學(xué)可能發(fā)現(xiàn)了前面提到內(nèi)容中的就有相關(guān)的方法:
- (void)tableNode:(ASTableNode *)tableNode willBeginBatchFetchWithContext:(ASBatchContext *)context;
現(xiàn)在絕大多數(shù)APP加載更多數(shù)據(jù)的方法都是通過(guò)下拉到列表底部再去請(qǐng)求數(shù)據(jù)然后添加到列表中缴允,但是 Texture 提供了另外一種更“合理”的方式,原文是這樣描述的:
By default, as a user is scrolling, when they approach the point in the table or collection where they are 2 “screens” away from the end of the current content, the table will try to fetch more data.
當(dāng)列表滾到到距離底部還有兩個(gè)屏幕高度請(qǐng)求新的數(shù)據(jù)练般,這個(gè)閾值是可以調(diào)整的矗漾。一旦距離底部達(dá)到兩個(gè)屏幕的高度的時(shí)候,就會(huì)調(diào)用前面提到的方法敞贡。所以用起來(lái)大概是這樣的:
- (void)tableNode:(ASTableNode *)tableNode willBeginBatchFetchWithContext:(ASBatchContext *)context{
[context beginBatchFetching];
[listApi startWithBlockSuccess:^(HQHomeListApi *request) {
@strongify(self);
NSArray *array = [request responseJSONObject];
[self.dataSourceArray addObjectsFromArray:array];
[self.tableNode insertSections:[NSIndexSet indexSetWithIndexesInRange:rang] withRowAnimation:UITableViewRowAnimationNone];
[self updateHavMore:array];
[context completeBatchFetching:YES];
} failure:NULL];
}
- (BOOL)shouldBatchFetchForTableNode:(ASTableNode *)tableNode{
return self.haveMore;
}
shouldBatchFetchForTableNode
用來(lái)控制是否需要獲取更多數(shù)據(jù)。這種方式優(yōu)點(diǎn)在于在網(wǎng)絡(luò)狀況好的情況下用戶都不會(huì)感受到已經(jīng)加載了其他數(shù)據(jù)并顯示摄职,缺點(diǎn)在于網(wǎng)絡(luò)狀況不好的情況下用于即使列表已經(jīng)下拉到底部也沒(méi)有任何提示