一天一點xib:10說說原理、優化方面的東西吧

引言

本來“一天一點xib”系列就九篇文章,但在留言中有一個朋友提出了兩點疑問:

1.為什么獲得重用cell的時候用的是dequeueReusableCellWithIdentifier:而沒有用dequeueReusableCellWithIdentifier: forIndexPath:這個函數。

2.為什么從xib獲得cell的時候用的是loadNibNamed: owner: options:而不是registerNib: forCellReuseIdentifier:這個函數?

其實這兩個問題歸根結底是一個問題,我當時考慮寫該系列文章的初衷就是想讓大家能慢慢了解xib,并喜歡上xib,所以當時并沒打算向大家講解Nib的生命周期是什么樣的、蘋果是如何優化Nib的等一系列理論性強的東西,怕大家認為xib過于復雜。

后面寫高冷的xib的時候我的初衷沒有變,還是希望大家能喜歡xib,所以還是從實用角度向大家講解xib的用法,上面的兩個問題要詳細解釋就要涉及到一個類——UINib,要講這個類,我想有必要對原理、或細節加以解釋,下面就來好好的說一說。這里再次感謝coderzcj這個朋友的留言反饋。

從nib的生命周期談起

這里為什么用詞是nib而不是xib,大家看一天一點xib:2初識xib就明白了。nib生命周期在官方文檔中有詳細說明,這里我們簡單總結一下:

1.將nib文件加載入內存

我們之前講過一種方式就是用bundle對象的loadNibNamed: owner: options:方法:

TestView *aTestView = [[NSBundle mainBundle] loadNibNamed:@"TestView" owner:self options:nil][0];

其實還有另一種更好的方式完成這個過程,那就是用UINib,這個我們后面會重點說。

2.“解固化”并實例化出nib文件里對應的對象

關于“固化”和“解固化”在一天一點xib:2初識xib中也有說明,這個過程中會調用initWithCoder: 方法,注意不會再調用 initWithFrame: 方法了,步驟1雖然將nib加載到了內存,但是它還是“數據”的形式,而這一步把步驟1中的“數據”變成了“對象”。

3.建立connections(outlets、actions)

outlets與actions就是我們之前提到的建立的IBOutlet與IBAction的“連接”。建立connections的順序是先建立outliets連接,然后建立actions連接。outlet建立的過程用到了setValue:forKey:方法,同時建立outlet的過程也是支持KVO的,如果你有一個屬性:

@property (strong, nonatomic) IBOutlet UIView *testView

那么你就可以注冊該屬性,通過KVO的回調得知outlet建立關系的時刻:

[self addObserver:self forKeyPath:@"testView" options:NSKeyValueObservingOptionInitial | NSKeyValueObservingOptionNew context:nil];

這里注意:options必須有NSKeyValueObservingOptionInitial,因為這是初始化階段,必須有NSKeyValueObservingOptionInitial才會發生回調,只用NSKeyValueObservingOptionNew是沒有回調發生的,只有初始化之后再重新賦值的時候用NSKeyValueObservingOptionNew才會發生回調。

action關系的建立就是調用UIControl類的addTarget:action:forControlEvents:方法。我們給xib上的一個button建立一個outlet,屬性名字叫testBtn,再建立一個action,函數名字叫btnPressed,則xib的source code中就會將兩者放在<connections>標簽中,nib加載到步驟3的時候就會根據這個標簽去建立對應的關系。

4.調用awakeFromNib方法:

- (void)awakeFromNib {
    [super awakeFromNib]; //這個函數要先調super
    //...做一些初始化之后的事情
    //注意該函數只會在綁定xib的類中調用,不會在它的File's Owner及其內部的Object類中調用
}
5.將xib中可見的控件顯示出來。

loadNibNamed: owner: options:的問題

了解了生命周期之后,再看我們平時用bundle對象的loadNibNamed: owner: options:方法會產生什么問題。如果我們顯示一個tableView,一般正常情況手機屏幕可以同時顯示8-10個cell(假設cell用了xib,而且cell的類型都是一樣的),那么就會加載8-10個cell,每個cell加載過程都會走上面的1-5個步驟,能不能把這個過程優化一下呢?

我們來分析一下cell加載的5個步驟:步驟1:頻繁的加載文件到內存,肯定有效率的問題。由于cell的類型都是樣的,所以加載的xib文件是一個,這里是可以優化的,因為每次加載到內存的xib文件是相同的,所以可以把它緩存到內存中,每次加載cell的時候步驟1直接用緩存。蘋果就是這樣優化的,據說效率比優化前提升2倍,如何把xib文件緩存起來?

答案是:UINib

bundle與UINib對比

bundle對象無法與xib文件產生映射關系,所以每次加載cell都是讀文件,而UINib對象與xib文件是映射關系,它就是內存中的xib文件,圖中的兩條紅色箭頭是理解的關鍵,所以我們可以通過UINib對象來達到緩存的目的

UINIb使用

UINib是iOS4就已經存在的類了,但是整個優化邏輯是iOS5、iOS6才完善的,UINib提供的方法很簡單:

NS_CLASS_AVAILABLE_IOS(4_0) @interface UINib : NSObject 

+ (UINib *)nibWithNibName:(NSString *)name bundle:(nullable NSBundle *)bundleOrNil;

+ (UINib *)nibWithData:(NSData *)data bundle:(nullable NSBundle *)bundleOrNil;

- (NSArray *)instantiateWithOwner:(nullable id)ownerOrNil options:(nullable NSDictionary *)optionsOrNil;
@end

我們通常使用nibWithNibName: bundle: 這個方法來初始化,name就是xib的名稱,bundle一般就是[NSBundle mainBundle],或者傳nil系統自動就幫你指定mainBundle了。

instantiateWithOwner: options: 方法的參數和我們之前說的loadNibNamed: owner: options:中的參數是一樣的

學習就是不斷修正對事物認識的過程

我們之前在講loadNibNamed: owner: options:方法的時候屏蔽了好多細節,目的是為了幫助大家在入門的時候好理解,現在是時候詳細的把這個函數說明白了。

再說loadNibNamed: owner: options:

name參數就是xib的名字,owner當時說默認傳self,其實這個owner就是指File's Owner,因為一般情況都是在File's Owner類中初始化話該xib的。

下面說最難說明白的參數options(一般傳nil),還記得高冷的xib中的有個object的用法嗎?是不是感覺很厲害?這個參數可以執行更厲害的。

我們建一個Xib10Demo的程序,建立PersonHandle類,繼承自NSObject,建立PersonView類,繼承自UIView,再建立PersonView.xib文件,在Identity inspector中指定class為PersonView,把view弄的小一點,設置個背景

在PersonView.xib中添加一個按鈕,再添加IBAction事件:

@implementation PersonView
- (IBAction)personBtnPressed:(id)sender {
    NSLog(@"%@:我的button點擊了", NSStringFromClass([self class]));
}
@end

在PersonHandle中添加如下代碼:

@implementation PersonHandle
- (IBAction)personHandle:(id)sender {
    NSLog(@"%@可以處理personView中的button點擊事件", NSStringFromClass([self class]));
}
@end

給PersonView.xib添加File's Owner:ViewController,再把里面的按鈕拖動到ViewController中,添加IBAction事件:

- (IBAction)personViewBtnPressed:(id)sender {
    NSLog(@"%@:我的PersonView上的button點擊了", NSStringFromClass([self class]));
}

在ViewController類中使用PersonView:

- (void)viewDidLoad {
    [super viewDidLoad];
    PersonView *aPersonView = [[NSBundle mainBundle] loadNibNamed:@"PersonView" owner:self options:nil][0];
    [self.view addSubview:aPersonView];
}

運行程序,點擊按鈕輸出:

2016-01-28 22:02:38.673 Xib10Demo[772:29149] PersonView:我的button點擊了
2016-01-28 22:02:38.674 Xib10Demo[772:29149] ViewController:我的PersonView上的button點擊了

到這里一切正常,下面就是見證options參數奇跡的時刻!

選擇PersonView.xib文件,在控件選擇區域中選擇External Object,并把它拖到左邊欄中,在Identify inspector中將class設置成為PersonHandle,在Attributes inspector中將Identifier設置成testKey。再把PersonHandle中的personHandle:方法與PersonView里的button建立IBAction的“連線”

修改ViewController類中的代碼:

@implementation ViewController{
    PersonHandle *_aPersonHandle;
}

- (void)viewDidLoad {
    [super viewDidLoad];
    _aPersonHandle = [PersonHandle new];
    NSDictionary *pramaDic = @{@"testKey" : _aPersonHandle}; //這里注意傳入這個dic中的_aPersonHandle對象必須是全局變量
    NSDictionary *optionDic = @{UINibExternalObjects : pramaDic};
    PersonView *aPersonView = [[NSBundle mainBundle] loadNibNamed:@"PersonView" owner:self options:optionDic][0];
    //options這個參數只接收一個key為UINibExternalObjects的字典,這個字典的value也必須是個字典,這個value字典的key就是xib中設置的external object的identifier
    [self.view addSubview:aPersonView];
}

再運行,點擊按鈕輸出:

2016-01-28 22:29:49.767 Xib10Demo[885:45056] PersonView:我的button點擊了
2016-01-28 22:29:49.768 Xib10Demo[885:45056] PersonHandle可以處理personView中的button點擊事件
2016-01-28 22:29:49.768 Xib10Demo[885:45056] ViewController:我的PersonView上的button點擊了

這就是options參數的魔力,我無法用語言說明它到底是干什么的,希望通過這個例子大家自己去體會。

問題來了:這有什么用?

具體應用總是被你的思路所局限住,你的思路有多廣,xib就有多靈活

給VC瘦身是我們每個人iOS程序員都在考慮的事情,你有沒有想過把它和xib結合起來?有好多人是把tableView的delegate和datasource處理抽出到另一個類中從而減少VC代碼,你有沒有想過這個過程靠xib來實現?
看這樣一個UI:

我們假設交互是這樣的:我的 | 推薦 這里點擊后選中的item會變色,另一個保持黑色,底部的指示條會會有滑動的動畫,并且停止時有阻尼效果(左右滑動衰減最終停下),下面的整個View隨著item的切換左右發生切換,并刷新數據。

我這里給大家一個路:既然 我的 | 推薦 這個View需求這么多,就從VC中抽出來,用一個帶xib的view管理,類似于我們剛剛例子中的PersonView,它的交互和動畫效果可以在自己類的IBAction函數中執行,而點擊后下面整個View左右切換可以由VC自己來完成,畢竟這是它主要顯示的View,方式就通過Files's Owner中的IBAction函數來執行,數據的顯示就涉及到了tableView的delegate和datasource了,我們這里把這部分功能從VC中分出來,給一個繼承自NSObject的類管理就行,類似于我們上面例子中的PersonHandle,它處理的事件就可以通過options參數的形式來實現,這里屏蔽了細節的實現,只說了說思路,僅僅起一個拋磚引玉的效果,希望大家能夠靈活運用xib。

最后說說loadNibNamed: owner: options:方法返回NSArray的問題,之前和他家說固定取[0]就行,現在要再詳細的說說了,最早的xib中是只允許有一個對象的,所以固定取[0]就行,后來我們可以向一個xib中拖多個對象,建立一個TwoViews.xib文件,向其中拖入兩個對象:

xib中有兩個對象
NSArray *twoViewsArr = [[NSBundle mainBundle] loadNibNamed:@"TwoViews" owner:nil options:nil];
NSLog(@"twoViewArr:%@", twoViewsArr);

打印結果:

2016-01-28 23:03:21.034 Xib10Demo[959:58681] twoViewArr:(
    "<UIView: 0x7fb1f8d073a0; frame = (0 0; 100 100); autoresize = W+H; layer = <CALayer: 0x7fb1f8d42570>>",
    "<UIImageView: 0x7fb1f8d49070; frame = (0 0; 200 200); autoresize = RM+BM; userInteractionEnabled = NO; layer = <CALayer: 0x7fb1f8d10cb0>>"
)

由此可以看出,如果你有多個對象的話,這個數組就不能固定取[0]了,要通過每個對象的Class,或者tag來區分你要用哪個對象。

這一點給我的啟發是:可以把UI類似的對象放在一個xib文件中集中管理。

回到UINib

剛剛說了那么多關于loadNibNamed: owner: options:參數的問題就是為了說UINib的參數問題,因為他們的參數是一樣的。

有了這些鋪墊UINib用法就好說多了,一般來說UINib有兩種用法:第一種就是用instantiateWithOwner: options:函數直接實例化對象使用,第二種是用registerNib:系列方法

用instantiateWithOwner: options:函數直接實例化對象

這個函數就不講了,和上面用法一樣,我們這里還是用最普通的用法,都傳nil就行。假設我們有個VC中有個tableView,加載的cell都是一種樣式的自定義cell,這里假設叫PersonCell,PersonCell類有一個PersonCell.xib文件,那么:

#import "SecondController.h"
#import "PersonCell.h"
@interface SecondController ()<UITableViewDelegate, UITableViewDataSource>
@end

@implementation SecondController {
    UINib *_personNib;
}

- (void)viewDidLoad {
    [super viewDidLoad];
    _personNib = [UINib nibWithNibName:@"PersonCell" bundle:nil]; //這句話就相當于把PersonCell.xib放到了內存里
}

- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
    return 20;
}

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
    UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"person"];
    if (!cell) {
        cell = [_personNib instantiateWithOwner:nil options:nil][0];//最初屏幕顯示的cell都沒有重用,都是要創建的,但是此時創建的過程都是走的內存緩存,大大提高了效率
    }
    return cell;
}
@end

第二種是用registerNib:系列方法

registerNib:系列方法都是UITableView的,主要有:

- (void)registerNib:(nullable UINib *)nib forCellReuseIdentifier:(NSString *)identifier NS_AVAILABLE_IOS(5_0);
- (void)registerNib:(nullable UINib *)nib forHeaderFooterViewReuseIdentifier:(NSString *)identifier NS_AVAILABLE_IOS(6_0);
- (void)registerClass:(nullable Class)aClass forHeaderFooterViewReuseIdentifier:(NSString *)identifier NS_AVAILABLE_IOS(6_0);

如果我們把自定義cell對應的UINib對象注冊到這些函數中去,也起到了緩存的效果,而且在返回UITableViewCell的函數中要調用dequeueReusableCellWithIdentifier: forIndexPath:這個方法來代替之前調用的dequeueReusableCellWithIdentifier: 方法,而且不需要判斷cell如果是nil的話在創建cell的邏輯,為什么不需要這段邏輯了呢?

- (nullable __kindof UITableViewCell *)dequeueReusableCellWithIdentifier:(NSString *)identifier; 
- (__kindof UITableViewCell *)dequeueReusableCellWithIdentifier:(NSString *)identifier forIndexPath:(NSIndexPath *)indexPath NS_AVAILABLE_IOS(6_0);

我們注意看這兩個函數的返回值:nullable表示可以是空值,所以我們用它的時候如果沒有cell的話要自己創建,而下面函數返回值中并沒有nullable,證明它一定會存在,不需要我們再實現判斷cell的邏輯了,__kindof用來說明返回值是UITableViewCell或者它的子類,類似于泛型,比之前用的id類型再強轉要好多了。

之前OC是沒有這些符號的,是swift推出后為了和swift在語言設計上保持一致,OC新版本中加入的,具體是哪個版本不清楚,但是是隨著swift2和xcode7發布而出來的,這不是我們這篇文章討論的問題,下面來看看具體用法:

#import "SecondController.h"
#import "PersonCell.h"
@interface SecondController ()<UITableViewDelegate, UITableViewDataSource>
@end

@implementation SecondController {
    UINib *_personNib;
    __weak IBOutlet UITableView *_pTableView;
}

- (void)viewDidLoad {
    [super viewDidLoad];
    _personNib = [UINib nibWithNibName:@"PersonCell" bundle:nil]; //這句話就相當于把PersonCell.xib放到了內存里
    [_pTableView registerNib:_personNib forCellReuseIdentifier:@"person"];
    //上面就是把_personNib注冊到tableView里,由于后面并沒有cell的判斷,我們可以猜測這個函數其實就是用傳入的_personNib參數實例化cell并把它放在重用池里,這樣就一定存在cell,就不需要判斷cell是否存在了。
    //這里其實也是有優化思想存在的,如果我們頻繁在調用返回cell的函數中創建cell,會產生降低效率的隱患,所以我們提前把nib注冊到tableView里,到時候直接從重用池中取就好了,但是注意這種用法的話,重用池中一定存在多個重用id相同的cell,此時就要用indexPath進行判斷到底取哪個,這也就是為什么當我們用registerNib的時候蘋果讓我們用dequeueReusableCellWithIdentifier: forIndexPath: 函數替換dequeueReusableCellWithIdentifier:的原因。
    //本質上這種優化是把后面頻繁多次調用datasource方法里要做的事情前置了。
}

- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
    return 20;
}

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
    UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"person" forIndexPath:indexPath];
    return cell;
}
@end

注意上面代碼中viewDidLoad里的注釋是關鍵,是核心,要理解。

總結

講了很多枯燥的理論,希望有助于大家更好的理解這些問題,也感謝大家對我的支持,能給我的反饋,我會繼續努力,寫出更好的文章給大家,多謝。

歡迎大家和我交流溝通,文章中有任何錯誤和漏洞,懇請指正,謝謝。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容