序
猿題庫是一個擁有數(shù)千萬用戶的創(chuàng)業(yè)公司,從2013年題庫項目起步到2015年,團(tuán)隊保持了極高的生產(chǎn)效率,使我們的產(chǎn)品完成了五個大版本和數(shù)十個小版本的高速迭代。在如此快速的開發(fā)過程中,如何保證代碼的質(zhì)量,降低后期維護(hù)的成本,以及為項目越來越快的版本迭代速度提供支持,成為了我們關(guān)注的重要問題。這篇文章將闡明我們在猿題庫 iOS 客戶端的架構(gòu)設(shè)計。
MVC
MVC,Model-View-Controller,我們從這個古老而經(jīng)典的設(shè)計模式入手。采用 MVC 這個架構(gòu)的最大的優(yōu)點在于其概念簡單,易于理解,幾乎任何一個程序員都會有所了解,幾乎每一所計算機(jī)院校都教過相關(guān)的知識。而在 iOS 客戶端開發(fā)中,MVC 作為官方推薦的主流架構(gòu),不但 SDK 已經(jīng)為我們實現(xiàn)好了 UIView、UIViewController 等相關(guān)的組件,更是有大量的文檔和范例供我們參考學(xué)習(xí),可以說是一種非常通用而成熟的架構(gòu)設(shè)計。
但 MVC 也有他的壞處。由于 MVC 的概念過于簡單樸素,已經(jīng)越來越難以適應(yīng)如今客戶端的需求,大量的代碼邏輯在 MVC 中并沒有定義得很清楚究竟應(yīng)該放在什么地方,導(dǎo)致他們很容易就會堆積在 Controller 里,成為了人們所說的 Massive View Controller。
MVVM
MVVM,Model-View-ViewModel,一個從 MVC 模式中進(jìn)化而來的設(shè)計模式,最早于2005年被微軟的 WPF 和 Silverlight 的架構(gòu)師 John Gossman 提出。在 iOS 開發(fā)中實踐 MVVM 的話,通常會把大量原來放在 ViewController 里的視圖邏輯和數(shù)據(jù)邏輯移到 ViewModel 里,從而有效的減輕了 ViewController 的負(fù)擔(dān)。另外通過分離出來的 ViewModel 獲得了更好的測試性,我們可以針對 ViewModel 來測試,解決了界面元素難于測試的問題。MVVM 通常還會和一個強(qiáng)大的綁定機(jī)制一同工作,一旦 ViewModel 所對應(yīng)的 Model 發(fā)生變化時,ViewModel 的屬性也會發(fā)生變化,而相對應(yīng)的 View 也隨即產(chǎn)生變化。
同樣的,MVVM 也有他的缺點:
一個首要的缺點是,MVVM 的學(xué)習(xí)成本和開發(fā)成本都很高。MVVM 是一個年輕的設(shè)計模式,大多數(shù)人對他的了解都不如 MVC 熟悉,基于綁定機(jī)制來進(jìn)行編程需要一定的學(xué)習(xí)才能較好的上手。同時在 iOS 客戶端開發(fā)中,并沒有現(xiàn)成的綁定機(jī)制可以使用,要么使用 KVO,要么引入類似 ReactiveCocoa 這樣的第三方庫,使得學(xué)習(xí)成本和開發(fā)成本進(jìn)一步提高。
另一個缺點是,數(shù)據(jù)綁定使 Debug 變得更難了。數(shù)據(jù)綁定使程序異常能快速的傳遞到其他位置,在界面上發(fā)現(xiàn)的 Bug 有可能是由 ViewModel 造成的,也有可能是由 Model 層造成的,傳遞鏈越長,對 Bug 的定位就越困難。
同時還必須指出的是,在傳統(tǒng)的 MVVM 架構(gòu)中,ViewModel 依然承載的大量的邏輯,包括業(yè)務(wù)邏輯,界面邏輯,數(shù)據(jù)存儲和網(wǎng)絡(luò)相關(guān),使得 ViewModel 仍然有可能變得和 MVC 中 ViewController 一樣臃腫。
在兩種架構(gòu)中權(quán)衡而產(chǎn)生的架構(gòu)
兩種架構(gòu)的優(yōu)點都想要,缺點又都想避開,我們在兩種架構(gòu)中權(quán)衡了他們的優(yōu)缺點,設(shè)計出了一個新的架構(gòu),起了一個名字叫:MVVM without Binding with DataController,架構(gòu)圖如下:

ViewModel
先來看右邊視圖相關(guān)的部分,傳統(tǒng)的 MVC 當(dāng)中 ViewController 中有大量的數(shù)據(jù)展示和樣式定制的邏輯,我們引入 MVVM 中 ViewModel 的概念,將這部分視圖邏輯移到了 ViewModel 當(dāng)中。在這個設(shè)計中,每一個 View 都會有一個對應(yīng)的 ViewModel,其包含了這個 View 數(shù)據(jù)展示和樣式定制所需要的所有數(shù)據(jù)。同時,我們不引入雙向綁定機(jī)制或者觀察機(jī)制,而是通過傳統(tǒng)的代理回調(diào)或是通知來將 UI 事件傳遞給外界。而 ViewController 只需要生成一個 ViewModel 并把這個裝配給對應(yīng)的 View,并接受相應(yīng)的 UI 事件即可。
這樣做有幾個好處:首先是 View 的完全解耦合,對于 View 來說,只需要確定好相應(yīng)的 ViewModel 和 UI 事件的回調(diào)接口即可與 Model 層完全隔離;而 ViewController 可以避免與 View 的具體表現(xiàn)打交道,這部分職責(zé)被轉(zhuǎn)交給了 ViewModel,有效的減輕了 ViewController 的負(fù)擔(dān);同時我們棄用了傳統(tǒng)綁定機(jī)制,使用了傳統(tǒng)的易于理解的回調(diào)機(jī)制來傳遞 UI 事件,降低了學(xué)習(xí)成本,同時使得數(shù)據(jù)的流入和流出變得易于觀察和控制,降低了維護(hù)了調(diào)適的成本。
DataController
接下來我們關(guān)注 Model 和 VC 之間的關(guān)系。如之前提到,在傳統(tǒng)的 MVVM 中,ViewModel 接管了 ViewController 的大部分職責(zé),包括數(shù)據(jù)獲取,處理,加工等等,導(dǎo)致其很有可能變得臃腫。我們將這部分邏輯抽離出來,引入一個新的部件,DataController。
ViewController 可以向 DataController 請求獲取或是操作數(shù)據(jù),也可以將一些事件傳遞給 DataController,這些事件可以是 UI 事件觸發(fā)的。DataController 在收到這些請求后,再向 Model 層獲取或是更新數(shù)據(jù),最后再將得到的數(shù)據(jù)加工成 ViewController 最終需要的數(shù)據(jù)返回。
這樣做之后,使得數(shù)據(jù)相關(guān)的邏輯解耦合,數(shù)據(jù)的獲取、修改、加工都放在 Data Controller 中處理,View Controller 不關(guān)心數(shù)據(jù)如何獲得,如何處理,Data Controller 也不關(guān)心界面如何展示,如何交互。同時 Data Controller 因為完全和界面無關(guān),所以可以有更好的測試性和復(fù)用性。
DataController 層和 Model 層之間的界限并不是僵硬的,但需要保證每一個 ViewController 都有一個對應(yīng)的 DataController。Data Controller 更強(qiáng)調(diào)的是其作為業(yè)務(wù)邏輯對外的接口。而在 DataController 中調(diào)用更底層的 Model 層邏輯是我們推薦的編程范式,例如數(shù)據(jù)加工層,網(wǎng)絡(luò)層,持久層等。
在后面的例子中,我們會更詳細(xì)的講解 DataController 的實現(xiàn)細(xì)節(jié)。
Show me the code
我們以猿題庫主頁為例,展示我們是如何使用應(yīng)用這個架構(gòu)的。
主頁有幾個部分組成,最上面的小猴子 Banner 頁,用于滾動展示一些活動信息;中間有一個用戶名字的頁面,用于展示用戶信息和答題情況以及一些心靈雞湯;最底下的這部分是一個課目選擇頁面,展示了用戶開啟的科目入口,在更多選項里面可以進(jìn)一步配置這些科目入口。接下來我們會以科目頁面(SubjectView)為例展示一些細(xì)節(jié)。
ViewController
我們會給每一個 ViewController 都創(chuàng)建一個對應(yīng)的 DataController。 例如我們給主頁建一個類起名叫APEHomePraticeViewController,同時他會有一個對應(yīng)的 DataController 起名叫APEHomePraticeDataController。同時我們把頁面拆分為幾個部分,每個部分有一個相對應(yīng)的 SubView。代碼如下:
1234567891011
@interface APEHomePracticeViewController () @property (nonatomic, strong, nullable) UIScrollView *contentView;@property (nonatomic, strong, nullable) APEHomePracticeBannerView *bannerView;@property (nonatomic, strong, nullable) APEHomePracticeActivityView *activityView;@property (nonatomic, strong, nullable) APEHomePracticeSubjectsView *subjectsView;@property (nonatomic, strong, nullable) APEHomePracticeDataController *dataController;@end
在viewDidLoad的時候,初始化好各個 SubView,并設(shè)置好布局:
1234567891011121314
- (void)setupContentView {self.contentView = [[UIScrollView alloc] init];[self.view addSubview:self.contentView];self.bannerView = [[APEHomePracticeBannerView alloc] init];self.activityView = [[APEHomePracticeActivityView alloc] init];self.subjectsView = [[APEHomePracticeSubjectsView alloc] init];self.subjectsView.delegate = self;[self.contentView addSubview:self.bannerView];[self.contentView addSubview:self.activityView];[self.contentView addSubview:self.subjectsView];// Layout Views ...}
接下來,ViewController 會向 DataController 請求 Subject 相關(guān)的數(shù)據(jù),并在請求完成后,用獲得的數(shù)據(jù)生成 ViewModel,將其裝配給 SubjectView,完成界面渲染,代碼如下:
123456789101112
- (void)fetchSubjectData {[self.dataController requestSubjectDataWithCallback:^(NSError *error) {if (error == nil) {[self renderSubjectView];}}];}- (void)renderSubjectView {APEHomePracticeSubjectsViewModel *viewModel =[APEHomePracticeSubjectsViewModel viewModelWithSubjects:self.dataController.openSubjects];[self.subjectsView bindDataWithViewModel:viewModel];}
數(shù)據(jù)結(jié)構(gòu)
為了更好的演示,我們接下來要介紹一下 Subject 相關(guān)的數(shù)據(jù)結(jié)構(gòu):
APESubject是科目的資源結(jié)構(gòu),包含了 Subject 的 id 和 name 等資源屬性,這部分屬性是用戶無關(guān)的;APEUserSubject是用戶的科目信息,包含了用戶是否打開某個學(xué)科的屬性。
123456789101112131415
@interface APESubject : NSObject@property (nonatomic, strong, nullable) NSNumber *id;@property (nonatomic, strong, nullable) NSString *name;@end@interface APEUserSubject : NSObject@property (nonatomic, strong, nullable) NSNumber *id;@property (nonatomic, strong, nullable) NSNumber *updatedTime;///? On or Off@property (nonatomic) APEUserSubjectStatus status;@end
DataController
如我們之前所說,每一個 ViewController 都會有一個對應(yīng)的 DataController,這一類 DataController 的主要職責(zé)是處理這個頁面上的所有數(shù)據(jù)相關(guān)的邏輯,我們稱其為 View Related Data Controller。
12345678
// APEHomePracticeDataController.h@interface APEHomePracticeDataController : APEBaseDataController// 1@property (nonatomic, strong, nonnull, readonly) NSArray *openSubjects;// 2- (void)requestSubjectDataWithCallback:(nonnull APECompletionCallback)callback;@end
上面的這個代碼
我們定義了一個界面最終需要的數(shù)據(jù)的 property,這里是openSubjects,這個 property 會存儲用戶打開的科目列表,他的類型是APESubject。
我們還會定義一個接口來請求 openSubject 數(shù)據(jù)。
DataController 這一層是一個靈活性很高的部件,一個 DataController 可以復(fù)用更小的 DataController,這一類更小的 DataController 通常只會包含純粹的或是更抽象的 Model 相關(guān)的邏輯,例如網(wǎng)絡(luò)請求,數(shù)據(jù)庫請求,或是數(shù)據(jù)加工等。我們稱這一類 DataController 為 Model Related Data Controller。
Model Related Data Controller 通常會為上層提供正交的數(shù)據(jù):
12345678910111213141516171819202122
// APEHomePracticeDataController.m@interface APEHomePracticeDataController ()@property (nonatomic, strong, nonnull) APESubjectDataController *subjectDataController;@end@implementation APEHomePracticeDataController- (void)requestSubjectDataWithCallback:(nonnull APECompletionCallback)callback {APEDataCallback dataCallback = ^(NSError *error, id data) {callback(error);};[self.subjectDataController requestAllSubjectsWithCallback:dataCallback];[self.subjectDataController requestUserSubjectsWithCallback:dataCallback];}- (nonnull NSArray *)openSubjects {return self.subjectDataController.openSubjectsWithCurrentPhase ?: @[];}@end
在我們的APEHomePraticeDataController的實現(xiàn)中,就包含了一個APESubjectDataController,這個subjectDataController會負(fù)責(zé)請求 All Subjects 和 User Subjects,并將其加工成上層所最終需要的 Open Subjects。(備注:這個例子里面的 callback 會回調(diào)多次是猿題庫產(chǎn)品的需求,如有需要,可在這一層控制請求都完成后再調(diào)用上層回調(diào))
事實上,Model Related Data Controller 可以一般性的認(rèn)為就是大家經(jīng)常在寫的 Model 層代碼,例如 UserAgent,UserService,PostService 之類的服務(wù)。之后讀者若想重構(gòu)就項目成這個架構(gòu),大可以不必糾結(jié)于形式,直接在 DataController 里調(diào)用舊有代碼的邏輯即可,如圖下面這樣的行為都是允許的:
ViewModel
每一個 View 都會有一個對應(yīng)的 ViewModel,這個 ViewModel 會包含展示這個 View 所需要的所有數(shù)據(jù)。
我們會使用工廠方法來創(chuàng)建 View Model,例如這個例子里,Subject View Model 不需要關(guān)心傳遞給他是什么樣的 Subject,所有的課目或者只是用戶開啟的科目。
1234567891011
@interface APEHomePracticeSubjectsViewModel : NSObject@property (nonatomic, strong, nonnull) NSArray*cellViewModels;@property (nonatomic, strong, nonnull) UIColor *backgroundColor;+ (nonnull APEHomePracticeSubjectsViewModel *)viewModelWithSubjects:(nonnull NSArray*)subjects;@end
ViewModel 可以包含更小的 ViewModel,就像 View 可以有 SubView 一樣。SubjectView 的內(nèi)部是由一個UICollectionView實現(xiàn)的,所以我們也給了對應(yīng)的 Cell 設(shè)計了一個 ViewModel。
需要額外注意的是,ViewModel 一般來說會包含的顯示界面所需要的所有元素,但粒度是可以控制。一般來說,我們只把會因為業(yè)務(wù)變化而變化的部分設(shè)為 ViewModel 的一部分,例如這里的 titleColor 和 backgroundColor 會因為主題不同而變化,但字體的大小(titleFont)卻是不會變的,所以不需要事無巨細(xì)的都加到 ViewModel 里。
12345678910111213
@interface APEHomePracticeSubjectsCollectionCellViewModel : NSObject@property (nonatomic, strong, nonnull) UIImage *image;@property (nonatomic, strong, nonnull) UIImage *highlightedImage;@property (nonatomic, strong, nonnull) NSString *title;@property (nonatomic, strong, nonnull) UIColor *titleColor;@property (nonatomic, strong, nonnull) UIColor *backgroundColor;+ (nonnull APEHomePracticeSubjectsCollectionCellViewModel *)viewModelWithSubject:(nonnullAPESubject *)subject;+ (nonnull APEHomePracticeSubjectsCollectionCellViewModel *)viewModelForMore;@end
View
View 只需要定義好裝配 ViewModel 的接口和定義好 UI 回調(diào)事件即可:
123456789101112131415
@protocol APEHomePracticeSubjectsViewDelegate - (void)homePracticeSubjectsView:(nonnull APEHomePracticeSubjectsView *)subjectViewdidPressItemAtIndex:(NSInteger)index;@end@interface APEHomePracticeSubjectsView : UIView@property (nonatomic, strong, nullable, readonly) APEHomePracticeSubjectsViewModel *viewModel;@property (nonatomic, weak, nullable) id delegate;- (void)bindDataWithViewModel:(nonnull APEHomePracticeSubjectsViewModel *)viewModel;@end
渲染界面的時候,完全依靠 ViewModel 進(jìn)行,包括 View 的 SubView 也會使用 ViewModel 里面的子 ViewModel 渲染。
1234567891011121314151617
- (void)bindDataWithViewModel:(nonnull APEHomePracticeSubjectsViewModel *)viewModel {self.viewModel = viewModel;self.backgroundColor = viewModel.backgroundColor;[self.collectionView reloadData];[self setNeedsUpdateConstraints];}- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath {APEHomePracticeSubjectsCollectionViewCell *cell = [collectionViewdequeueReusableCellWithReuseIdentifier:@"Cell" forIndexPath:indexPath];if (0 <= indexPath.row && indexPath.row < self.viewModel.cellViewModels.count) {APEHomePracticeSubjectsCollectionCellViewModel *vm =self.viewModel.cellViewModels[indexPath.row];[cell bindDataWithViewModel:vm];}return cell;}
至此,我們就完成了所有的步驟。我們回過頭再看一下 ViewController 的職責(zé)就回變的非常簡單,裝配好 View,向 DataController 請求數(shù)據(jù),裝配 ViewModel,配置給 View,接收 View 的UI事,一切復(fù)雜的操作都能夠的代理出去。
總結(jié)
優(yōu)點
通過上面的例子我們可以看到,這個架構(gòu)有幾個優(yōu)點:
層次清晰,職責(zé)明確:和界面有關(guān)的邏輯完全劃到 ViewModel 和 View 一遍,其中 ViewModel 負(fù)責(zé)界面相關(guān)邏輯,View 負(fù)責(zé)繪制;Data Controller 負(fù)責(zé)頁面相關(guān)的數(shù)據(jù)邏輯,而 Model 還是負(fù)責(zé)純粹的數(shù)據(jù)層邏輯。 ViewController 僅僅只是充當(dāng)簡單的膠水作用。
耦合度低,測試性高:除開 ViewController 外,各個部件可以說是完全解耦合的,各個部分也是可以完全獨(dú)立測試的。同一個功能,可以分別由不同的開發(fā)人員分別進(jìn)行開發(fā)界面和邏輯,只需要確立好接口即可。
復(fù)用性高:解耦合帶來的額外好處就是復(fù)用性高,例如同一個View,只需要多一個工廠方法生成 ViewModel,就可以直接復(fù)用。數(shù)據(jù)邏輯代碼不放在 ViewController 層也可以更方便的復(fù)用。
學(xué)習(xí)成本低: 本質(zhì)上來說,這個架構(gòu)屬于對 MVC 的優(yōu)化,主要在于解決 Massive View Controller 問題,把原本屬于 View Controller 的職責(zé)根據(jù)界面和邏輯部分相應(yīng)的拆到 ViewModel 和 DataController 當(dāng)中,所以是一個非常易于理解的架構(gòu)設(shè)計,即使是新手也可以很快上手。
開發(fā)成本低: 完全不需要引入任何第三方庫就可以進(jìn)行開發(fā),也避免了因為 MVVM 維護(hù)成本高的問題。
實施性高,重構(gòu)成本低:可以在 MVC 架構(gòu)上逐步重構(gòu)的架構(gòu),不需要整體重寫,是一種和 MVC 兼容的設(shè)計。
缺點
不可否認(rèn)的是,這個設(shè)計也有其相應(yīng)的缺點,由于其把傳統(tǒng) MVVM 里面的 VM 拆成兩部分,會照成下面的一些情況:
當(dāng)頁面的交互邏輯非常多時,需要頻繁的在 DC-VC-VM 里來回傳遞信息,造成了大量膠水代碼。
另外,由于在傳統(tǒng)的 MVVM 中 VM 原本是一體的,一些復(fù)雜的交互本來可以在 VM 中直接完成測試,如今卻需要同時使用 DC 和 VM 并附上一些膠水代碼才能進(jìn)行測試。
沒有了 Binding,代碼寫起來會更費(fèi)勁一點(仁者見仁,智者見智)。
后記
MVVM 是一個很棒的架構(gòu),私底下我也會用其來做一些個人項目,但在公司項目里,我會更慎重的考慮個中利弊。我做這個設(shè)計的時候,心儀 MVVM 的種種好處,又忌憚于它的種種壞處,再考慮到團(tuán)隊的開發(fā)和維護(hù)成本,所以最終設(shè)計成了如今這樣。
個人認(rèn)為,好的架構(gòu)設(shè)計的都是和團(tuán)隊以及業(yè)務(wù)場景息息相關(guān)的。我們這套架構(gòu)幫助我們解決了 ViewController 代碼堆積的問題,也帶來了更清晰明了的代碼層級和模塊職責(zé),同時沒有引入過多的復(fù)雜性。希望大家也能充分理解這套架構(gòu)的適用場景,在自己的 APP 架構(gòu)設(shè)計中有所借鑒。
Lancy
2015.12.30
Jan 6th, 2016
Variable argument lists 使用方法
可變參數(shù)函數(shù)(Variadic Function),即是指一個可以接受可變數(shù)量的參數(shù)的函數(shù)。在C語言中,對該特性的支持,即是通過可變參數(shù)列表(Variable Argument list)來實現(xiàn)的,其定義在stdarg.h頭文件。(若使用C++則在cstdarg頭文件)。
以如下C代碼為例說明,該函數(shù)接受可變數(shù)量的整數(shù)作為參數(shù),求和:
1234567891011121314151617181920
intaddemUp(intfirstNum,...){// 1. 參數(shù)后面添加省略號...va_listargs;// 2. 創(chuàng)建一個va_list類型的變量intsum=firstNum;intnumber;va_start(args,firstNum);// 3. 初始化va_list,此時va_list指向firstNum之后的第一個參數(shù)while(1){number=va_arg(args,int);// 4. 獲取當(dāng)前指向的參數(shù)的值,并移動到下一個參數(shù)sum+=number;if(number==0){// 用0表示結(jié)束break;}}va_end(args);// 5. 清理returnsum;}// 調(diào)用sum=addemUp(1,2,3,4,5,0);// sum = 15
要創(chuàng)建一個可變參數(shù)函數(shù),需要把一個省略號(…)放在函數(shù)的參數(shù)列表后面。
接著需要聲明一個一個va_list類型的變量,這個va_list類型的變量類似于一個指向參數(shù)的指針。
接著我們調(diào)用va_start()并傳入函數(shù)的最后一個聲明的參數(shù)的變量名,來使得va_list變量指向第一個附加的參數(shù)。
接著我們調(diào)用va_arg()并傳入我們期待的參數(shù)類型,程序就會返回與該類型匹配數(shù)量的字節(jié)(即參數(shù)的值),并且移動va_list指向下一個參數(shù)。之后不斷的調(diào)用va_arg(),獲得更多的參數(shù)的值,直到完成整個參數(shù)處理的過程。
最后調(diào)用va_end()來進(jìn)行清理。
variable argument lists 的內(nèi)部機(jī)制
如我們之前所說,當(dāng)我們調(diào)用va_start()并將va_list和函數(shù)最后定義的參數(shù)傳入時,實際上是將va_list內(nèi)在的一個指針指向函數(shù)調(diào)用棧 (call stack)中參數(shù)所在的區(qū)域的一端,每一次我們調(diào)用va_arg(),其都會根據(jù)提供的類型,返回當(dāng)前指針?biāo)赶虻牡刂烽_始對應(yīng)的字節(jié)數(shù)的數(shù)據(jù),即參數(shù)的值,并移動指針相應(yīng)字節(jié)數(shù)的距離。我們傳給va_arg()的類型,即是其用來判定需要取得得數(shù)據(jù)的大小,以及指針需要移動的距離。如圖描述了這個過程:
事實上,這是一個很危險的事情,你總是需要提供正確的類型來讓va_arg()正確執(zhí)行,而且va_arg()并不知道何時停止,你需要提供一個標(biāo)記或一個參數(shù)的總數(shù)來停止va_arg()繼續(xù)執(zhí)行。若你提供了不正確的類型,或者沒有在該停止的時候停止,你將會獲得不可預(yù)測的值,并且很有可能導(dǎo)致程序崩潰。
解決方案
一般而言,為了確保參數(shù)的獲取正確進(jìn)行,有如下兩種解決方案:
Format string
如C語言中的printf,Cocoa中的NSLog,[NSString stringWithFormat:]就是使用了Format String的解決方案。通常,該函數(shù)的第一個參數(shù)既為一個format string,函數(shù)內(nèi)部實現(xiàn)會掃描這個format string,來確定之后接著的可變參數(shù)的數(shù)量和類型。例如:
1
NSString*str=[NSStringstringWithFormat:@"int %d, str %@, float %g",123,@"ok",123.4];
這里使用了%作為轉(zhuǎn)義符,其后跟著的d代表int,@代表id,g代表float/double,這表示后面必須有三個參數(shù),其類型必須與format string所指定的一致。
如之前所說,提供的參數(shù)的數(shù)量或者類型若與提供的format string不一致,則會發(fā)生不可預(yù)知的問題。而在運(yùn)行的時候,我們沒有任何的辦法去保證其正確性,幸運(yùn)的是編譯器提供了一些方法,能讓我們在編譯的時候做一些檢查:
gcc中定義了__attribute__((format))來標(biāo)示一個可變參函數(shù)使用了format string,從而在編譯時對其進(jìn)行檢查。其定義為format (archetype, string-index, first-to-check),其中archetype代表format string的類型,它可以是printf,scanf,strftime或者strfmon,Cocoa開發(fā)者還可以使用__NSString__來指定其使用和[NSString stringWithFormat:]與NSLog()一致的format string規(guī)則。string-index代表format string是第幾個參數(shù),first-to-check則代表了可變參數(shù)列表從第幾個參數(shù)開始。示例:
12345
// 第一個參數(shù)是format,第二個參數(shù)起是可變參數(shù)列表,format的格式規(guī)則與printf一致voidcustomPrintf(constchar*format,...)__attribute__((format(printf,1,2)));// 使用的時候,若format和參數(shù)不符,則會報warningcustomPrintf("what? %d",1.2,2);
Cocoa開發(fā)者可以使用NS_FORMAT_FUNCTION(F,A)宏來替代__atribute__format,F(xiàn)和A即對應(yīng)string-index和first-to-check,事實上,他的實現(xiàn)類似于:
1
#define NS_FORMAT_FUNCTION(F,A) __attribute__((format(__NSString__, F, A)))
示例如下:
1
FOUNDATION_EXPORTvoidNSLog(NSString*format,...)NS_FORMAT_FUNCTION(1,2);
Sentinel value
哨兵值是另一種可變參數(shù)列表所常用的方案,如前一節(jié)我們的示例代碼,即是使用了數(shù)字0作為哨兵值。當(dāng)程序發(fā)現(xiàn)當(dāng)前讀取到的參數(shù)值為0時,則停止繼續(xù)讀取程序。在Cocoa中,我們經(jīng)常使用nil作為哨兵值,比如[NSArray arrayWithObjects:]方法,其接受數(shù)量不等的對象作為參數(shù),而在最后則必須使用nil結(jié)尾。如:
12
[NSArrayarrayWithObjects:@1,@2,@3,nil];//備注:我們現(xiàn)在通常使用@[@1, @2, @3]來代替這一行代碼,且不需要在最后添加nil,這稱為字面量(Literals)
同format string一樣危險的是,若開發(fā)者調(diào)用方法(函數(shù))的時候,忘記在最后添加上哨兵值,則會發(fā)生不可預(yù)知的問題。同樣幸運(yùn)的是,編譯器也為我們提供了一些方法來在編譯時進(jìn)行檢查。
gcc中定義了___attribute__((sentinel))來標(biāo)示一個函數(shù)需要在編譯的時候?qū)ι诒颠M(jìn)行檢查。用法如下:
1
intaddemUp(intfirstNum,...)__attribute__((sentinel));
Cocoa開發(fā)者可以使用NS_REQUIRES_NIL_TERMINATION宏來替代,其實現(xiàn)基本等同于上述代碼:
1
+(instancetype)arrayWithObjects:(id)firstObj,...NS_REQUIRES_NIL_TERMINATION;
工程實例
我在開發(fā)猿題庫iOS客戶端時,由于產(chǎn)品的需要會有許多alert彈框。但傳統(tǒng)的UIAlertView經(jīng)常需要實現(xiàn)相應(yīng)的UIAlertViewDelegate,使用起來非常不便。我寫了一個能夠接收block作為回調(diào)的自定義的AlertView組件,同時為了保證其接口與UIAlertView基本一致,使用了可變參數(shù)列表。其接口定義如下:
123456789
@interfaceCYAlertView:UIAlertView-(id)initWithTitle:(NSString*)titlemessage:(NSString*)messageclickedBlock:(void(^)(CYAlertView*alertView,BOOLcancelled,NSIntegerbuttonIndex))clickedBlockcancelButtonTitle:(NSString*)cancelButtonTitleotherButtonTitles:(NSString*)otherButtonTitles,...NS_REQUIRES_NIL_TERMINATION;@end
完整的代碼開源托管在GitHub(傳送門),有興趣的同學(xué)可以參考。
聯(lián)系我
水平有限,若有任何關(guān)于該文章的疑問或者指正,歡迎和我討論
寫郵件:lancy1014#gmail.com
關(guān)注我的微博
Fo我的Github
在這里寫評論留言
參考
Clang 3.5 documentation: Attributes in Clang
GCC documentation: Function Attributes
Advanced Mac OS X Programming
lancy
2014.5.12
May 5th, 2014
什么是 Toll-Free Bridging
有一些數(shù)據(jù)類型是能夠在 Core Foundation Framework 和 Foundation Framework 之間交換使用的。這意味著,對于同一個數(shù)據(jù)類型,你既可以將其作為參數(shù)傳入 Core Foundation 函數(shù),也可以將其作為接收者對其發(fā)送 Objective-C 消息(即調(diào)用ObjC類方法)。這種在 Core Foundation 和 Foundation 之間交換使用數(shù)據(jù)類型的技術(shù)就叫 Toll-Free Bridging.
舉例說明,NSString和CFStringRef即是一對可以相互轉(zhuǎn)換的數(shù)據(jù)類型:
1234567891011
// ARC 環(huán)境下// Bridging from ObjC to CFNSString*hello=@"world";CFStringRefworld=(__bridgeCFStringRef)(hello);NSLog(@"%ld",CFStringGetLength(world));// Bridging from CF to ObjCCFStringRefhello=CFStringCreateWithCString(kCFAllocatorDefault,"hello",kCFStringEncodingUTF8);NSString*world=(__bridgeNSString*)(hello);NSLog(@"%ld",world.length);CFRelease(hello);
大部分(但不是所有!)Core Foundation 和 Foundation 的數(shù)據(jù)類型可以使用這個技術(shù)相互轉(zhuǎn)換,Apple 的文檔里有一個列表(傳送門),列出了支持這項技術(shù)的數(shù)據(jù)類型。
MRC 下的 Toll-Free Bridging 因為不涉及內(nèi)存管理的轉(zhuǎn)移,可以直接相互 bridge 而不必使用類似__bridge修飾字,我們之后再討論這個問題。
Toll-Free Bridging 是如何實現(xiàn)的?
1.
每一個能夠 bridge 的 ObjC 類,都是一個類簇(class cluster)。類簇是一個公開的抽象類,但其核心功能的是在不同的私有子類中實現(xiàn)的,公開類只暴露一致的接口和實現(xiàn)一些輔助的創(chuàng)建方法。而與該 ObjC 類相對應(yīng)的 Core Foundation 類的內(nèi)存結(jié)構(gòu),正好與類簇的其中一個私有子類相同。
舉個例子,NSString是一個類簇,一個公開的抽象類,但每次創(chuàng)建一個NSString的實例時,實際上我們會獲得其中一個私有子類的實例。而NSString的其中一個私有子類實現(xiàn)既為NSCFString,其內(nèi)存的結(jié)構(gòu)與CFString是相同的,CFString的isa指針就指向NSCFString類,即,CFString對象就是一個NSCFString類的實例。
所以,當(dāng)NSString的實現(xiàn)剛好是NSCFString的時候,他們兩者之間的轉(zhuǎn)換是相當(dāng)容易而直接的,他們就是同一個類的實例。
2.
當(dāng)NSString的實現(xiàn)不是NSCFString的時候(比如我們自己 subclass 了NSString),我們調(diào)用 CF 函數(shù),就需要先檢查對象的具體實現(xiàn)。如果發(fā)現(xiàn)其不是NSCFString,我們不會調(diào)用 CF 函數(shù)的實現(xiàn)來獲得結(jié)果,而是通過給對象發(fā)送與函數(shù)功能相對應(yīng)的 ObjC 消息(調(diào)用相對應(yīng)的NSString的接口)來獲得其結(jié)果。
例如CFStringGetLength函數(shù),當(dāng)收到一個作為參數(shù)傳遞進(jìn)來的對象時,會先確認(rèn)該對象到底是不是NSCFString實現(xiàn)。如果是的話,就會直接調(diào)用CFStringGetLength函數(shù)的實現(xiàn)來獲得字符串的長度;如果不是的話,會給對象發(fā)送length消息(調(diào)用NSString的- (NSUInteger)length接口),來得到字符串的長度。
通過這樣的技術(shù),即使是我們自己子類了一個NSString,也可以和CFStringRef相互 Bridge。
3.
其他支持 Toll-Free Bridging 的數(shù)據(jù)類型原理也同NSString一樣,比如NSNumber的NSCFNumber和CFNumber。
ARC 下的 Toll-Free Bridging
如之前提到的,MRC 下的 Toll-Free Bridging 因為不涉及內(nèi)存管理的轉(zhuǎn)移,相互之間可以直接交換使用:
123456789
// bridgeNSString*nsStr=(NSString*)cfStr;CFStringRefcfStr=(CFStringRef)nsStr;// 調(diào)用函數(shù)或者方法NSUIntegerlength=[(NSString*)cfStrlength];NSUIntegerlength=CFStringGetLength((CFStringRef)nsStr);// releaseCFRelease((CFStringRef)nsStr);[(NSString*)cfStrrelease];
而在 ARC 下,事情就會變得復(fù)雜一些,因為 ARC 能夠管理 Objective-C 對象的內(nèi)存,卻不能管理 CF 對象,CF 對象依然需要我們手動管理內(nèi)存。在 CF 和 ObjC 之間 bridge 對象的時候,問題就出現(xiàn)了,編譯器不知道該如何處理這個同時有 ObjC 指針和 CFTypeRef 指向的對象。
這時候,我們需要使用__bridge,__bridge_retained,__bridge_transfer修飾符來告訴編譯器該如何去做。
__bridge
最常用的修飾符,這意味著告訴編譯器不做任何內(nèi)存管理的事情,編譯器仍然負(fù)責(zé)管理好在 Objc 一端的引用計數(shù)的事情,開發(fā)者也繼續(xù)負(fù)責(zé)管理好在 CF 一端的事情。舉例說明:
例子1
12345
// objc to cfNSString*nsStr=[selfcreateSomeNSString];CFStringRefcfStr=(__bridgeCFStringRef)nsStr;CFUseCFString(cfStr);// CFRelease(cfStr); 不需要
在這里,編譯器會繼續(xù)負(fù)責(zé)nsStr的內(nèi)存管理的事情,不會在 bridge 的時候 retain 對象,所以也不需要開發(fā)者在 CF 一端釋放。需要注意的是,當(dāng)nsStr被釋放的時候(比如出了作用域),意味著cfStr指向的對象被釋放了,這時如果繼續(xù)使用cfStr將會引起程序崩潰。
例子2
12345
// cf to objcCFStringRefhello=CFStringCreateWithCString(kCFAllocatorDefault,"hello",kCFStringEncodingUTF8);NSString*world=(__bridgeNSString*)(hello);CFRelease(hello);// 需要[selfuseNSString:world];
在這里,bridge 的時候編譯器不會做任何內(nèi)存管理的事情,bridge 之后,會負(fù)責(zé) ObjC 一端的內(nèi)存管理的事情 。同時,開發(fā)者需要負(fù)責(zé)管理 CF 一端的內(nèi)存管理的事情,需要再 bridge 之后,負(fù)責(zé) release 對象。
__bridge_retained
接__bridge一節(jié)的第一個例子,objc to cf。為了防止nsStr被釋放,引起我們使用cfStr的時候程序崩潰,可以使用__bridge_retained修飾符。這意味著,在 bridge 的時候,編譯器會 retain 對象,而由開發(fā)者在 CF 一端負(fù)責(zé) release。這樣,就算nsStr在 objc 一端被釋放,只要開發(fā)者不手動去釋放cfStr,其指向的對象就不會被真的銷毀。但同時,開發(fā)者也必須保證和負(fù)責(zé)對象的釋放。例如:
12345
// objc to cfNSString*nsStr=[selfcreateSomeNSString];CFStringRefcfStr=(__bridge_retainedCFStringRef)nsStr;CFUseCFString(cfStr);CFRelease(cfStr);// 需要
__bridge_transfer
接__bridge一節(jié)的第二個例子,cf to objc。我們發(fā)現(xiàn)如果使用__bridge修飾符在cf轉(zhuǎn)objc的時候非常的麻煩,我們既需要一個CFTypeRef的變量,還需要在 bridge 之后負(fù)責(zé)釋放。這時我們可以使用__bridge_transfer,意味著在 bridge 的時候,編譯器轉(zhuǎn)移了對象的所有權(quán),開發(fā)者不再需要負(fù)責(zé)對象的釋放。例如:
12345
// cf to objcCFStringRefhello=CFStringCreateWithCString(kCFAllocatorDefault,"hello",kCFStringEncodingUTF8);NSString*world=(__bridge_transferNSString*)(hello);// CFRelease(hello); 不需要[selfuseNSString:world];
甚至可以這么寫:
123
// cf to objcNSString*world=(__bridge_transferNSString*)CFStringCreateWithCString(kCFAllocatorDefault,"hello",kCFStringEncodingUTF8);[selfuseNSString:world];
小結(jié)
(__bridge T) op:告訴編譯器在 bridge 的時候不要做任何事情
(__bridge_retained T) op:( ObjC 轉(zhuǎn) CF 的時候使用)告訴編譯器在 bridge 的時候 retain 對象,開發(fā)者需要在CF一端負(fù)責(zé)釋放對象
(__bridge_transfer T) op:( CF 轉(zhuǎn) ObjC 的時候使用)告訴編譯器轉(zhuǎn)移 CF 對象的所有權(quán),開發(fā)者不再需要在CF一端負(fù)責(zé)釋放對象
聯(lián)系我
水平有限,若有任何關(guān)于該文章的疑問或者指正,歡迎和我討論
寫郵件:lancy1014#gmail.com
關(guān)注我的微博
Fo我的Github
在這里寫評論留言
參考
Concepts in Objective-C Programming
Core Foundation Design Concepts
Clang documentation: Objective-C Automatic Reference Counting (ARC)
Lancy
4.21
Apr 21st, 2014
故事
哥哥家的貓咪有一天迷上了風(fēng)靡全球的拼詞游戲Letterpress,但是貪吃的小貓咪只認(rèn)識“food”和“milk”這樣的詞語,所以經(jīng)常被對面的玩家欺負(fù)。可憐的小貓咪向哥哥求助:“喵嗚~哥哥~哥哥,他欺負(fù)我!”,于是充滿愛心和正義感的哥哥就踏上了拯救貓咪的道路。
開始拯救世界
唔,我們馬上來做一個自動拼詞器,拼詞器必須實現(xiàn)這樣的功能:
貓咪只需要選擇一張游戲截圖,拼詞器能自動識別游戲提供的字母。(記住:小喵掌是用不了鍵盤的哦
拼詞器根據(jù)識別出來的字母,自動拼出所有可能的單詞,并按長度由長到短排序顯示。(小貓咪就能方便的挑選單詞啦
有了這樣的工具,連貓咪都能玩拼詞游戲啦!
全部的代碼在Github開源托管:點這里
正式的開始
我們會使用到Xcode5,并創(chuàng)建一個iOS7的應(yīng)用。我將用到CoreGraph來做圖像處理,你需要一些圖像處理的基本常識,一些C語言的能力以及一點內(nèi)存管理的知識。
現(xiàn)在開始吧!
首先創(chuàng)建一個新的Xcode工程,模板選擇單頁面即可,名字就叫LetterFun(或者任何你和你的貓咪喜歡的名字),設(shè)備選擇iPhone,其他的選項讓你家貓咪決定。
接下來創(chuàng)建一個繼承自NSObject的類CYLetterManager,我們將用它來識別游戲截圖里面的字母。在頭文件加上這些方法:
12345678
// CYLetterManager.h@interfaceCYLetterManager:NSObject-(id)initWithImage:(UIImage*)image;\\1-(void)trainingWihtAlphabets:(NSArray*)array;\\2-(NSArray*)ocrAlphabets;\\3@end
我們假定一個CYLetterManager的實例只處理一個圖片,所以我們使用一個initWithImage:的方法,來確保需要我們處理的圖片總是被事先載入。
trainingWihtAlphabets:是一個訓(xùn)練方法,我們?nèi)斯ぽd入識別后的字母來讓其進(jìn)行訓(xùn)練,以提供后續(xù)字母識別的樣本。
ocrAlphabets從圖片里識別字母。
接著開始實現(xiàn)CYLetterManager。首先申明一些需要使用的變量:
123456
// CYLetterManager.m@implementationCYLetterManager{CGImageRef*_tagImageRefs;UIImage*_image;CGImageRef*_needProcessImage;}
其中_image是我們從initWithImage:里初始化得到的圖像,其他兩個變量,我會在后面用到的時候解釋。
實現(xiàn)初始化方法:
123456789
-(id)initWithImage:(UIImage*)image{self=[superinit];if(self){_image=image;[selfgetNeedProcessImages];}returnself;}
接著實現(xiàn)getNeedProcessImages,這個方法用來將原圖片切分為25個字母的小塊,并存入_needProcessImage數(shù)組內(nèi)。
12345678910111213141516171819202122232425262728293031
-(void)getNeedProcessImages{// 1CGImageReforiginImageRef=[_imageCGImage];CGImageRefalphabetsRegionImageRef=CGImageCreateWithImageInRect(originImageRef,CGRectMake(0,CGImageGetHeight(originImageRef)-640,640,640));CGFloatwidth=640;CGFloatheight=640;CGFloatblockWidth=width/5.0;CGFloatblockHeight=height/5.0;// 2 create image blocksCGImageRef*imagesRefs=malloc(25*sizeof(CGImageRef));for(NSIntegeri=0;i<5;i++){for(NSIntegerj=0;j<5;j++){CGRectalphabetRect=CGRectMake(j*blockWidth,i*blockHeight,blockWidth,blockHeight);CGImageRefalphabetImageRef=CGImageCreateWithImageInRect(alphabetsRegionImageRef,alphabetRect);imagesRefs[i*5+j]=alphabetImageRef;}}// 3 transform to binaryImagefor(NSIntegeri=0;i<25;i++){CGImageRefbinaryImage=[selfcreateBinaryCGImageFromCGImage:imagesRefs[i]];CGImageRelease(imagesRefs[i]);imagesRefs[i]=binaryImage;}// 4_needProcessImage=imagesRefs;CGImageRelease(alphabetsRegionImageRef);}
我們觀察游戲截圖,發(fā)現(xiàn)字母所在的區(qū)域在下方的640 * 640。我們使用CGImageCreateWithImageInRect函數(shù)創(chuàng)建了alphabetsRegionImageRef。注意:你需要使用CGImageRelease來release這個對象(函數(shù)最后一行),而originImageRef是由UIImage的CGImage方法獲得的,你并不持有它,故而不需要release。
我們把a(bǔ)lphabetsRegionImageRef裁剪成了25個小的方塊,暫時存在imagesRefs數(shù)組。
彩色圖片包含的信息太多,為了方便我們后續(xù)的處理,我們將得到的字母小方塊進(jìn)行二值化。注意:這里我們使用了自定義的函數(shù)createBinaryCGImageFromCGImage創(chuàng)建了一個二值化的image,再將其替換到數(shù)組里前,需要將數(shù)組里存在的舊對象release。
最后我們將imagesRefs賦值給_needProcessImage,并release不需要imageRef。
再來看如何進(jìn)行圖像二值化,先將這幾個常數(shù)加到initWithImage:方法的上面:
1234
constintRED=0;constintGREEN=1;constintBLUE=2;constintALPHA=3;
之后來實現(xiàn)createBinaryCGImageFromCGImage方法,從這里開始我們將涉及到像素的操作:
1234567891011121314151617181920212223242526272829303132333435363738
-(CGImageRef)createBinaryCGImageFromCGImage:(CGImageRef)imageRef{NSIntegerwidth=CGImageGetWidth(imageRef);NSIntegerheight=CGImageGetHeight(imageRef);CGRectimageRect=CGRectMake(0,0,width,height);// 1UInt32*pixels=(UInt32*)malloc(width*height*sizeof(UInt32));CGColorSpaceRefcolorSpace=CGColorSpaceCreateDeviceRGB();CGContextRefcontextA=CGBitmapContextCreate(pixels,width,height,8,width*sizeof(UInt32),colorSpace,kCGBitmapByteOrder32Big|kCGImageAlphaPremultipliedLast);CGContextDrawImage(contextA,imageRect,imageRef);// 2for(NSIntegery=0;y255){rgbaPixel[RED]=255;rgbaPixel[GREEN]=255;rgbaPixel[BLUE]=255;}else{rgbaPixel[RED]=0;rgbaPixel[GREEN]=0;rgbaPixel[BLUE]=0;}}}// 3CGImageRefresult=CGBitmapContextCreateImage(contextA);CGContextRelease(contextA);CGColorSpaceRelease(colorSpace);free(pixels);returnresult;}
使用CGBitmapContextCreate創(chuàng)建了一個 bitmap graphics context,并將 pixels 設(shè)為其 data pointer,再將 image 繪制到 context 上,這樣我們可以通過操作 pixels 來直接操作 context 的數(shù)據(jù)。該方法的其他參數(shù)可以參考文檔,參數(shù)會影響數(shù)據(jù),在這里請先使用我提供的參數(shù)。
我們遍歷了圖像的每個像素點對每個點進(jìn)行二值化,二值化有許多種算法,大體分為固定閥值和自適應(yīng)閥值兩類。這里我們觀察待處理圖片可知,我們需要提取的字母部分是明顯的黑色,這樣使用固定的閥值255,即可順利將其提取,而有顏色的部分會被剔除。
使用CGBitmapContextCreateImage來從context創(chuàng)建處理后的圖片,并清理數(shù)據(jù)。
注意:由于c沒有autorelease池,你應(yīng)當(dāng)在函數(shù)(方法)的命名上使用create(或copy)來提醒使用者應(yīng)當(dāng)負(fù)責(zé) release 對象。
至此,我們已經(jīng)完成了字母方塊的提取和二值化。為了防止我們沒出問題,來檢查一下成果。
將一張游戲截圖”sample.png”拖進(jìn)Xcode proj內(nèi)。
在CYViewController的viewDidLoad里使用該圖片實例化一個CYLetterManager。
在CYLetterManager的getNeedProcessImages里的任意地方加上斷點,可以是二值化前后,也可以是切小字母塊前后。
運(yùn)行!然后隆重介紹Xcode5的新功能之一,快速預(yù)覽,當(dāng)當(dāng)當(dāng)當(dāng)!
以本文最開始的截圖為例:
可以看到我們已經(jīng)成功的截出了第一個字母,并把其轉(zhuǎn)為二值化圖片。
下一步
載入了需要的圖片和進(jìn)行了預(yù)處理之后,我們來進(jìn)行識別的前奏:獲得識別用的樣本。為此我們實現(xiàn)trainingWihtAlphabets方法:
123456789
-(void)trainingWihtAlphabets:(NSArray*)array{for(NSIntegeri=0;i<25;i++){if(array[i]){[selfwriteImage:_needProcessImage[i]withAlphabet:array[i]];}}[selfprepareTagImageRefs];}
該方法接受一個字母數(shù)組,里面應(yīng)該包含著,我們之前載入圖片里的,從左到右,從上到下的字母隊列。比如@[@"t", @"e", @"j", ... , @"h"];
我們使用writeImage:withAlphabet:方法,將該圖片設(shè)為標(biāo)準(zhǔn)樣本,寫入到文件中。讀寫CGImageRef的方法如下:
12345678910111213141516171819202122232425262728293031323334353637383940414243
@importImageIO;@importMobileCoreServices;-(NSString*)pathStringWithAlphabet:(NSString*)alphabet{NSString*imageName=[alphabetstringByAppendingString:@".png"];NSString*documentsPath=[@"~/Documents"stringByExpandingTildeInPath];NSString*path=[documentsPathstringByAppendingString:[NSStringstringWithFormat:@"/%@",imageName]];returnpath;}-(CGImageRef)createImageWithAlphabet:(NSString*)alphabet{NSString*path=[selfpathStringWithAlphabet:alphabet];CGImageRefimage=[selfcreateImageFromFile:path];returnimage;}-(CGImageRef)createImageFromFile:(NSString*)path{CFURLRefurl=(__bridgeCFURLRef)[NSURLfileURLWithPath:path];CGDataProviderRefdataProvider=CGDataProviderCreateWithURL(url);CGImageRefimage=CGImageCreateWithPNGDataProvider(dataProvider,NULL,NO,kCGRenderingIntentDefault);CGDataProviderRelease(dataProvider);returnimage;}-(void)writeImage:(CGImageRef)imageRefwithAlphabet:(NSString*)alphabet{NSString*path=[selfpathStringWithAlphabet:alphabet];[selfwriteImage:imageReftoFile:path];}-(void)writeImage:(CGImageRef)imageReftoFile:(NSString*)path{CFURLRefurl=(__bridgeCFURLRef)[NSURLfileURLWithPath:path];CGImageDestinationRefdestination=CGImageDestinationCreateWithURL(url,kUTTypePNG,1,NULL);CGImageDestinationAddImage(destination,imageRef,nil);if(!CGImageDestinationFinalize(destination)){NSLog(@"Failed to write image to %@",path);}CFRelease(destination);}
prepareTagImageRefs方法將磁盤里保存的樣本圖片摘出來,存在_tagImageRefs數(shù)組里面,用于之后的比對。實現(xiàn)如下:
123456789101112
-(void)prepareTagImageRefs{_tagImageRefs=malloc(26*sizeof(CGImageRef));for(NSIntegeri=0;i<26;i++){charch='a'+i;NSString*alpha=[NSStringstringWithFormat:@"%c",ch];_tagImageRefs[i]=[selfcreateImageWithAlphabet:alpha];if(_tagImageRefs[i]==NULL){NSLog(@"Need sample: %c",ch);}}}
將[self prepareTagImageRefs]加到initWitImage:方法里面,這樣我們每次實例化的時候,都會自動從磁盤里讀取標(biāo)記好的樣本圖片。
非常需要注意的是:我們添加dealloc方法(用慣了arc的開發(fā)者可能會不習(xí)慣),但這是c,是需要我們自己管理內(nèi)存的。在dealloc里面釋放我們的成員變量吧:
12345678910111213
-(void)dealloc{for(NSIntegeri=0;i<26;i++){if(_tagImageRefs[i]!=NULL){CGImageRelease(_tagImageRefs[i]);}}free(_tagImageRefs);for(NSIntegeri=0;i<25;i++){CGImageRelease(_needProcessImage[i]);}free(_needProcessImage);}
接下來,我們需要載入足夠多的包含了26個英文字母的sample圖片,做好訓(xùn)練,將26個樣品圖片就都裁剪好的存入磁盤啦!(哥哥寫不動了,訓(xùn)練代碼在CYViewController里面,翻到最下面看源碼啦)
識別字母!
OCR技術(shù)從最早的模式匹配,到現(xiàn)在流行的特征提取,有各種各樣的方法。我們這里不搞那么復(fù)雜,而使用最簡單粗暴的像素比對。即我們之前將其轉(zhuǎn)化為二值化圖像了之后,直接比對兩個圖片相同的像素點比例即可。
我們使用標(biāo)記過的_tagImageRefs作為比對樣本,將要識別的圖像與26個標(biāo)準(zhǔn)樣本進(jìn)行比對,當(dāng)相似度大于某個閥值的時候,我們即判定其為某個字母,實現(xiàn)如下:
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162
-(NSString*)ocrCGImage:(CGImageRef)imageRef{NSIntegerresult=-1;for(NSIntegeri=0;i<26;i++){CGImageReftagImage=_tagImageRefs[i];if(tagImage!=NULL){CGFloatsimilarity=[selfsimilarityBetweenCGImage:imageRefandCGImage:tagImage];if(similarity>0.92){result=i;break;}}}if(result==-1){returnnil;}else{charch='a'+result;NSString*alpha=[NSStringstringWithFormat:@"%c",ch];returnalpha;}}// suppose imageRefA has same size with imageRefB-(CGFloat)similarityBetweenCGImage:(CGImageRef)imageRefAandCGImage:(CGImageRef)imageRefB{CGFloatsimilarity=0;NSIntegerwidth=CGImageGetWidth(imageRefA);NSIntegerheight=CGImageGetHeight(imageRefA);CGRectimageRect=CGRectMake(0,0,width,height);UInt32*pixelsOfImageA=(UInt32*)malloc(width*height*sizeof(UInt32));UInt32*pixelsOfImageB=(UInt32*)malloc(width*height*sizeof(UInt32));CGColorSpaceRefcolorSpace=CGColorSpaceCreateDeviceRGB();CGContextRefcontextA=CGBitmapContextCreate(pixelsOfImageA,width,height,8,width*sizeof(UInt32),colorSpace,kCGBitmapByteOrder32Big|kCGImageAlphaPremultipliedLast);CGContextRefcontextB=CGBitmapContextCreate(pixelsOfImageB,width,height,8,width*sizeof(UInt32),colorSpace,kCGBitmapByteOrder32Big|kCGImageAlphaPremultipliedLast);CGContextDrawImage(contextA,imageRect,imageRefA);CGContextDrawImage(contextB,imageRect,imageRefB);NSIntegersimilarPixelCount=0;NSIntegerallStrokePixelCount=0;for(NSIntegery=0;y
有了上面兩個識別的方法,我們再實現(xiàn)ocrAlphabets方法就很容易了:
12345678910111213
-(NSArray*)ocrAlphabets{NSMutableArray*alphabets=[NSMutableArrayarrayWithCapacity:25];for(NSIntegeri=0;i<25;i++){NSString*alphabet=[selfocrCGImage:_needProcessImage[i]];if(alphabet){[alphabetsaddObject:alphabet];}else{[alphabetsaddObject:@"unknown"];}}return[alphabetscopy];}
開始拼詞
首先,我們需要準(zhǔn)備一個詞典。你可以在Unix(或者Unix-like)的系統(tǒng)里找到words.txt這個文件,他一般存在/usr/share/dict/words, or /usr/dict/words
將這個文件拷貝出來,并添加到我們的工程里。我們將創(chuàng)建一個CYWordHacker類來做拼詞的事情,實現(xiàn)傳入一組字符,返回所有合法單詞按長度降序排列的數(shù)組的接口,如下:
123
@interfaceCYWordHacker:NSObject-(NSArray*)getAllValidWordWithAlphabets:(NSArray*)alphabets;@end
具體實現(xiàn)從略,可參照源碼。
界面
做成下面這樣就可以了:
界面細(xì)節(jié)大家就去看源碼吧~寫不動了~哥哥要和貓咪玩樂去了~
最終成品
全部的代碼在Github開源托管:點這里
還有一件事
這個東西其實到這里并不是就完了,我們將圖片二值化后其實去掉了圖片的很多信息,比如當(dāng)前游戲的狀態(tài)。有興趣的筒子,可以根據(jù)字塊的顏色,來識別出游戲的狀態(tài),寫出更智能更強(qiáng)力拼詞器。實現(xiàn)諸如:占有更多對方的格子或者做出最大的block區(qū)域等強(qiáng)力功能,甚至求出最優(yōu)解策略。這就涉及到人工智能的領(lǐng)域啦。
聯(lián)系我
寫郵件:lancy1014#gmail.com
關(guān)注我的微博
Fo我的Github
在這里寫評論留言
Lancy
20 Oct.
Oct 19th, 2013
介紹
位操作是程序設(shè)計中對位模式或二進(jìn)制數(shù)的一元和二元操作. 在許多古老的微處理器上, 位運(yùn)算比加減運(yùn)算略快, 通常位運(yùn)算比乘除法運(yùn)算要快很多. 在現(xiàn)代架構(gòu)中, 情況并非如此:位運(yùn)算的運(yùn)算速度通常與加法運(yùn)算相同(仍然快于乘法運(yùn)算).(摘自wikipedia)
OC作為c的擴(kuò)展和超集,位運(yùn)算自然使用的是c的操作符。c提供了6個位操作符,$,|,^,~,<<,>>。本文不打算做位運(yùn)算的基礎(chǔ)教學(xué),只介紹一些開發(fā)中能用到的場景。
提高運(yùn)算速度
如前一段所說,位運(yùn)算的運(yùn)算速度是通常與加法速度相當(dāng),但是快于乘法運(yùn)算的。故而如果我們的程序?qū)π阅苡幸螅覀兛梢允褂梦贿\(yùn)算來提高運(yùn)算速度。比如:
乘以2:n << 1;
除以2:n >> 1;
乘以2的m次方:n << m;
除以2的m次方:n >> m;
判斷奇偶:(n & 1) == 1;
求平均數(shù):(a + b) >> 1;
……
基于乘除法的位運(yùn)算提速還有很多,這里不一一列舉。需要注意的是,你應(yīng)當(dāng)只在遇到性能瓶頸的時候,并且瓶頸的確是計算的時候才這么做。因為使用位運(yùn)算并不利于程序的可讀性和可維護(hù)性。(科學(xué)計算除外)
壓縮空間
以前接觸過ACM的筒子們應(yīng)該對狀態(tài)壓縮不陌生,狀態(tài)壓縮的目的在于把一個大數(shù)據(jù)用有限的內(nèi)存空間來進(jìn)行表示。比如 Programming Pearls 里面的一個經(jīng)典示例:如何對最多有一千萬條不重復(fù)的7位整數(shù)(電話號碼)進(jìn)行排序?且可使用的內(nèi)存空間有大約1MB多。
顯而易見的常規(guī)做法既是做一個基于磁盤操作的外排序。然而如果轉(zhuǎn)換一下思路,充分的使用內(nèi)存中的每一個位,加上不存在重復(fù)的電話號碼,以及不存在0和1開頭的電話號碼。我們只需要使用1000萬個位(大約1.2mb),就能以集合的方式在內(nèi)存里標(biāo)記下所有的數(shù)據(jù),從而輕松的實現(xiàn)位排序。此種方法大幅度的減少了IO時間,從而獲得巨大的性能提升。
ACM里面有大量的如果使用位來壓縮空間的示例,狀態(tài)壓縮的動態(tài)規(guī)劃等,此處不做展開,只告訴讀者,充分的使用內(nèi)存的每一個位,經(jīng)常能帶來意想不到的收獲。但需要注意的是,狀態(tài)的壓縮和提取,都需要一定的計算量,有時一味的追求狀態(tài)壓縮,反而會降低效率。
表示數(shù)據(jù)
比較經(jīng)典的一個應(yīng)用場景,使用一串24位的十六機(jī)制數(shù)字來表現(xiàn)一個RGB顏色(或者32位來表示ARGB)。由于PS,Web以及各類取色器,都能快速的取出RGB的Hex值,但是UIColor沒有對應(yīng)的方法。故而我們可以寫出下面這樣一個UIColor的Category,來快速的用一個RGBHex生成一個UIColor。(源碼在UIColor + CYHelper.h)
12345678910111213
+(UIColor*)colorWithRGBHex:(UInt32)hex{return[UIColorcolorWithRGBHex:hexalpha:1.0f];}+(UIColor*)colorWithRGBHex:(UInt32)hexalpha:(CGFloat)alpha{intr=(hex>>16)&0xFF;intg=(hex>>8)&0xFF;intb=(hex)&0xFF;return[UIColorcolorWithRed:r/255.0fgreen:g/255.0fblue:b/255.0falpha:alpha];}
狀態(tài)與選項
1234567891011121314151617181920212223242526
typedefNS_OPTIONS(NSUInteger,UIViewAnimationOptions){UIViewAnimationOptionLayoutSubviews=1<<0,UIViewAnimationOptionAllowUserInteraction=1<<1,// turn on user interaction while animatingUIViewAnimationOptionBeginFromCurrentState=1<<2,// start all views from current value, not initial valueUIViewAnimationOptionRepeat=1<<3,// repeat animation indefinitelyUIViewAnimationOptionAutoreverse=1<<4,// if repeat, run animation back and forthUIViewAnimationOptionOverrideInheritedDuration=1<<5,// ignore nested durationUIViewAnimationOptionOverrideInheritedCurve=1<<6,// ignore nested curveUIViewAnimationOptionAllowAnimatedContent=1<<7,// animate contents (applies to transitions only)UIViewAnimationOptionShowHideTransitionViews=1<<8,// flip to/from hidden state instead of adding/removingUIViewAnimationOptionOverrideInheritedOptions=1<<9,// do not inherit any options or animation typeUIViewAnimationOptionCurveEaseInOut=0<<16,// defaultUIViewAnimationOptionCurveEaseIn=1<<16,UIViewAnimationOptionCurveEaseOut=2<<16,UIViewAnimationOptionCurveLinear=3<<16,UIViewAnimationOptionTransitionNone=0<<20,// defaultUIViewAnimationOptionTransitionFlipFromLeft=1<<20,UIViewAnimationOptionTransitionFlipFromRight=2<<20,UIViewAnimationOptionTransitionCurlUp=3<<20,UIViewAnimationOptionTransitionCurlDown=4<<20,UIViewAnimationOptionTransitionCrossDissolve=5<<20,UIViewAnimationOptionTransitionFlipFromTop=6<<20,UIViewAnimationOptionTransitionFlipFromBottom=7<<20,}NS_ENUM_AVAILABLE_IOS(4_0);
我們觀察Apple在UIViewAnimationOptions的枚舉變量,使用了一個NSUInteger就表示了UIViewAnimation所需的所有Option。其中0~9十個是互不影響的可同時存在option。16~19,20~24使用了4位來表示互斥的option。
如此定義了之后,對UIViewAnimationOptions的賦值變得尤為簡單,使用 | 操作符既可以獲得一個給對應(yīng)的option位賦值后的結(jié)果。例如:
1234567
[UIViewanimateWithDuration:1.0delay:0options:UIViewAnimationOptionAllowUserInteraction|UIViewAnimationOptionBeginFromCurrentState|UIViewAnimationOptionCurveEaseInanimations:{...}completion:{...}];
提取也比較簡單,使用 & 操作符 和 >> 操作符,就可以輕松判定某個位有沒有被設(shè)置,以及提取某些狀態(tài)位,例如:
12345678910111213141516171819
UIViewAnimationOptionsoption=UIViewAnimationOptionAllowUserInteraction|UIViewAnimationOptionBeginFromCurrentState|UIViewAnimationOptionCurveEaseIn|UIViewAnimationOptionTransitionCrossDissolve;if(option&UIViewAnimationOptionAllowUserInteraction){NSLog(@"UIViewAnimationOptionAllowUserInteraction has been set");}if(option&UIViewAnimationOptionBeginFromCurrentState){NSLog(@"UIViewAnimationOptionBeginFromCurrentState has been set");}UInt8optionCurve=option>>16&0xf;if(optionCurve==1){NSLog(@"UIViewAnimationOptionCurveEaseIn has been set");}UInt8optionTransition=option>>20&0xf;if(optionTransition==5){NSLog(@"UIViewAnimationOptionTransitionCrossDissolve has been set");}
這里最需要注意的地方就是,對互斥的狀態(tài)的設(shè)置必須尤為小心,如果你這么寫:
1234
UIViewAnimationOptionsbadOption=UIViewAnimationOptionCurveEaseIn|UIViewAnimationOptionCurveEaseOut;UInt8oops=badOption>>16&0xf;NSLog(@"Sorry, it's not UIViewAnimationOptionCurveEaseInOut");NSLog(@"oops = %d, you got UIViewAnimationOptionCurveLinear",oops);
聯(lián)系我
寫郵件:lancy1014#gmail.com
關(guān)注我的微博
Fo我的Github
在這里寫評論留言
Lancy
9.27
Sep 27th, 2013
前言
關(guān)于CocoaPods,相信不用我介紹更多了。本文主要介紹如何制作自己的CocoaPods spec。
步驟
首先你要會用git,還要有一個托管在云端的repo,本文以Github為例,Git和Github的使用方式參照Github Help
在你的repo下面,使用Git的tag功能,給你的某個commit添加一個tag(比如1.1.0),并push到Github.
// 本地添加一個標(biāo)簽:
$ git tag -a 1.1.0 -m "Version 1.1.0 Stable"
// Push tag to GitHub:
$ git push --tags
FolkCocoaPods/Specs并 Clone 到本地。
在Clone下來的Specs/創(chuàng)建一個自己的spec的目錄,再創(chuàng)建一個版本目錄。比如:
Specs/CYHelper/1.1.0
在該目錄下創(chuàng)建一個spec檔案,并編輯:
$ pod spec create CYHelper
$ vi CYHelper.podspec
pod創(chuàng)建模板會有相關(guān)的說明,按指引一步一步填即可。例如,CYHelper的spec配置如下:
Pod::Spec.new do |s|
s.name? ? ? ? = "CYHelper"
s.version? ? ? = "1.1.0"
s.summary? ? ? = "CYHelper is an Objective-C library for iOS developers."
s.homepage? ? = "https://github.com/lancy/CYHelper"
s.license? ? ? = 'MIT (LICENSE)'
s.author? ? ? = { "lancy" => "lancy1014@gmail.com" }
s.source? ? ? = { :git => "https://github.com/lancy/CYHelper.git", :tag => "1.1.0" }
s.platform? ? = :ios, '5.0'
s.source_files = 'CYHelper', 'CYHelper/**/*.{h,m}'
s.exclude_files = 'CYHelperDemo'
s.frameworks = 'Foundation', 'CoreGraphics', 'UIKit'
s.requires_arc = true
end
驗證podspec
pod spec lint CYHelper.podspec
如果驗證成功的話,會有這樣的提示
Analyzed 1 podspec.
CYHelper.podspec passed validation.
最后去Github上發(fā)一個PullRequest,等待一段時間的審核和Merge,之后就可以像別的pod那樣用CocoaPods來管理了:
// Podfile
platform :ios, '6.0'
pod 'CYHelper'
$ pod install
Have Fun!
后注
這里有唐巧和王軻寫的兩篇相關(guān)的文章,可以作為擴(kuò)展閱讀:
Aug 11th, 2013
Singletons in Cocoa, Are They Evil?
故事
這事是這樣的,去年我在上課的時候,和老師討論了一下關(guān)于架構(gòu)的問題,我是開發(fā)Cocoa/iOS的,老師是開發(fā)Web的,而老師是一個堅定的singletons are evil的擁護(hù)者,我和他說了我的App的架構(gòu),直接被他一頓猛劈,強(qiáng)烈的譴責(zé)了我使用Singletons,我回應(yīng)說,這個pattern在Cocoa里是大量使用的,結(jié)果被搞了一句“用的多的就是對的么?你回去多學(xué)習(xí)一下再來討論吧”。
于是我非常郁悶的回去搜索的一大頓的資料,還在Stackoverflow上發(fā)起了一個問題:singletons in cocoa, are they evil?。甚至在某個社區(qū),假扮singleton are evil的擁護(hù)者,把所有singleton的缺點列了一堆,結(jié)果又是群起而攻之一場舌戰(zhàn)。
關(guān)于Singleton的缺點,放出一段引用:
They are generally used as a global instance, why is that so bad? Because you hide the dependencies of your application in your code, instead of exposing them through the interfaces. Making something global to avoid passing it around is a code smell.
They violate the Single Responsibility Principle: by virtue of the fact that they control their own creation and lifecycle.
They inherently cause code to be tightly coupled. This makes faking them out under test rather difficult in many cases.
They carry state around for the lifetime of the app. Another hit to testing since you can end up with a situation where tests need to be ordered which is a big no no for unit tests. Why? Because each unit test should be independent from the other.
公說公有理,婆說婆有理,一度把我弄得越來越困惑,后來我看到這一段話,我就徹底釋然了:
As for degrees of evil – it’s like speech or literature. F-words are “evil”. If you speak constantly using f-words words the quality of your language is lower – people can’t tell if you want to say something or just swearing. But in some situations such words help you to get things done (think of the battlefield orders). It sort of the same thing with features/patterns and people who have to read and maintain their usage.
– hoha
BTW,今天我甚至看到了Accessors Are Evil這樣的東西,更堅定了我再也不相信xxx are evil這種說法的決心。
我現(xiàn)在認(rèn)為Design pattern是前人總結(jié)的經(jīng)驗,不同的設(shè)計模式有不同的優(yōu)缺點,比如說用工廠代替單例的,雖說解決了單例的一些問題,但你要真去寫一個工廠就知道有多蛋疼,多浪費(fèi)生命了。然而在較為大型的應(yīng)用,非常多人協(xié)作的項目,隊友對項目的把握不一致,水平有高低之分,這時工廠又反而是一種安全的,省時省力的做法。
其實在代碼的世界里面,你想要更多的安全,就會喪失更多的靈活性和便利性。如何在這中間取舍,就需要我們徹底的了解某種模式(或者說某種編程方法)的優(yōu)缺點,在保證基本的安全性的情況下,盡可能的減少工作量,提高工作效率。
Singletons in Cocoa
回到正題,還是來說說Cocoa上的單例。Cocoa中的普遍的,大部分的單例,并不是嚴(yán)格的單例(strict singleton),而是一種共享單例(shared singleton),例如sharedApplication,sharedURLCache等。即,大多數(shù)情況,我們訪問同一個類方法,就可以獲得一個同樣的實例,但若真的需要存在多個實例亦可。通常,共享單例使用一個shared開的類方法識別。只有當(dāng)真的只有唯一的一個共享資源的時候,或者不可能有多個資源的時候(比如GPS模塊),才會使用嚴(yán)格意義的共享單例。
線程安全的Singleton
絕大多數(shù)情況下,使用一個共享單例比使用共享單例要好,然而這里有一個常見的創(chuàng)建共享單例的錯誤,即使是Apple自己的開發(fā)者文檔也沒弄清楚的一個錯誤,他們把Singleton寫成了非線程安全的:
1234567
+(MyClass*)sharedInstance{staticMyClass*sharedInstance;if(sharedInstance==nil){sharedInstance=[[MyClassalloc]init];}returnsharedInstance;}
正確的寫法應(yīng)該是:
123456789
+(MyClass*)sharedInstance{staticMyClass*sharedInstance;@synchronized(self){if(sharedInstance==nil){sharedInstance=[[MyClassalloc]init];}}returnsharedInstance;}
更恰當(dāng)?shù)膶懛ㄊ鞘褂胐ispatch_once()
123456789
+(MYClass*)sharedInstance{staticdispatch_once_tpred=0;staticMYClass_sharedObject=nil;dispatch_once(&pred,^{_sharedObject=[[selfalloc]init];// or some other init method});return_sharedObject;}
dispatch_once()即為執(zhí)行且僅僅執(zhí)行某個block一次,他是同步的方法(記住GCD也有很多同步的方法),其速度也比 @synchronized 快許多。
嚴(yán)格的單例(strict singleton)
盡管我們很少會使用到嚴(yán)格的單例模式,但當(dāng)真的需要的時候,還是可以實現(xiàn)的。
蘋果官方文檔提供了一個嚴(yán)格單例的實現(xiàn)(傳送門)。 其重載了allocWithZone:, copyWithZone, retain, retainCount, release, autorelease。使得這個實現(xiàn)變得無比復(fù)雜而難以理解和控制。
而大多數(shù)情況下,實現(xiàn)嚴(yán)格的單例模式,只需要和共享單例相同的代碼,再使用NSAssert使得一切調(diào)用init的代碼作為一個錯誤處理即可,代碼如下:
1234567891011121314151617181920
+(MYSingleton*)sharedSingleton{staticdispatch_once_tpred;staticMYSingleton*instance=nil;dispatch_once(&pred,^{instance=[[selfalloc]initSingleton];});returninstance;}-(id)init{// Forbid calls to –init or +newNSAssert(NO,@”CannotcreateinstanceofSingleton”);// You can return nil or [self initSingleton] here,// depending on how you prefer to fail.returnnil;}// Real (private) init method-(id)initSingleton{self=[superinit];if((self=[superinit])){// Init code }returnself;}
這份代碼的優(yōu)點是很明顯的,避免了復(fù)雜的內(nèi)存操作和重載,又靜止了調(diào)用者創(chuàng)建多個實例。
小結(jié)
小結(jié)一下,單例模式是Cocoa中非常常用的一個模式,對于應(yīng)用程序中廣泛使用的對象,單例模式是非常便利的方法。而我們也應(yīng)當(dāng)在使用的時候多注意單例模式的一些缺點,盡可能的在實現(xiàn)的時候避免他們,比如讓單例不存在過于復(fù)雜的依賴性和繼承,保證其松耦合等。
Edit:
One more thing:有筒子問到是@synchronized(self)還是@synchronized(sharedInstance)?
答案是:均可。
self,在實例方法中表現(xiàn)是實例,這一點自不用多說。在類方法中則表現(xiàn)為一種多態(tài)的類實例(class instance),他總是會返回正確的類型,比如這樣:
1234
+(id)new{return[[selfalloc]init];}
而在本文的這個@synchronized(self)里的self,總是會指向同一個對象,即那個特殊的類實例。(class也是一個對象),故而此處可以使用self。
lancy
Jun 4th, 2013
OBJC中聲明字符串常量的一個常見錯誤(常量指針和指針常量)
我們知道,NSNotification是Cocoa中觀察模式最易用的實現(xiàn)方法,比起直接使用KVO(Key-Value Observing)他更加容易實現(xiàn)也更好理解。一個樣例:
Poster.h
12
// Define a string constant for the notificationexternNSString*constPosterDidSomethingNotification;
Poster.m
123456
NSString*constPosterDidSomethingNotification=@”PosterDidSomethingNotification”;...// Include the poster as the object in the notification[[NSNotificationCenterdefaultCenter]postNotificationName:PosterDidSomethingNotificationobject:self];
Observer.m
123456789101112131415161718
// Import Poster.h to get the string constant#import “Poster.h”...// Register to receive a notification[[NSNotificationCenterdefaultCenter]addObserver:selfselector:@selector(posterDidSomething:)name:PosterDidSomethingNotificationobject:nil];...-(void)posterDidSomething:(NSNotification*)note{// Handle the notification here}-(void)dealloc{// Always remove your observations[[NSNotificationCenterdefaultCenter]removeObserver:self];[superdealloc];}
注意到,在使用Notifikation的時候,會需要聲明字符串常量,作為notification的name。這時,const的位置就比較重要,很容易讓不了解的人犯錯誤:
錯誤的寫法(常量指針):
1
externconstNSString*RNFooDidCompleteNotification;
正確的寫法(指針常量):
1
externNSString*constRNFooDidCompleteNotification;
這里涉及到常量指針和指針常量的概念,簡單的來說:
常量指針:就是指向常量的指針,關(guān)鍵字 const 出現(xiàn)在 * 左邊,表示指針?biāo)赶虻牡刂返膬?nèi)容是不可修改的,但指針自身可變。
指針常量:指針自身是一個常量,關(guān)鍵字 const 出現(xiàn)在 * 右邊,表示指針自身不可變,但其指向的地址的內(nèi)容是可以被修改的。
在此例中:我們知道,NSString永遠(yuǎn)是immutable的,所以NSString * const 是有效的,而const NSString * 則是無效的。而使用錯誤的寫法,則無法阻止修改該指針指向的地址,使得本應(yīng)該是常量的值能被修改,造成了隱患。這是需要注意的一個常見錯誤。
Jun 2nd, 2013
Objective-C Associative References(關(guān)聯(lián)引用) 續(xù):相關(guān)實踐
About
我之前寫了一篇博文Objective-C Associative References(關(guān)聯(lián)引用),介紹我在在研究objc runtime的有趣的發(fā)現(xiàn),但當(dāng)時我并沒有意識到這個技術(shù)應(yīng)該使用在何處。在一些實踐之后,小結(jié)一下有關(guān)關(guān)聯(lián)引用的一些相關(guān)實踐吧。
Category中使用關(guān)聯(lián)引用來添加property
我們知道category是不能創(chuàng)建實例變量的,但我們可以通過關(guān)聯(lián)引用來達(dá)到這樣的目的。特別是當(dāng)你不持有這個類,比如說系統(tǒng)的類,而你又的確需要添加一個property。
你可以這樣做:
1234567891011121314151617
#import @interfacePerson(EmailAddress)@property(readwrite,copy)NSString*emailAddress;@end@implementationPerson(EmailAddress)staticcharemailAddressKey;-(NSString*)emailAddress{returnobjc_getAssociatedObject(self,&emailAddressKey);}-(void)setEmailAddress:(NSString*)emailAddress{objc_setAssociatedObject(self,&emailAddressKey,emailAddress,OBJC_ASSOCIATION_COPY);}@end
給UI控件關(guān)聯(lián)上相關(guān)對象
比如UIAlert只有一個tag屬性用來做標(biāo)記,我們經(jīng)常需要根據(jù)Tag屬性在找出對應(yīng)需要操作的對象。但使用關(guān)聯(lián)對象,我們可以把UIAlert和某個對象關(guān)聯(lián),簡化這個過程。
比如你可以這樣做:
12345678910
idinterestingObject=...;UIAlertView*alert=[[UIAlertViewalloc]initWithTitle:@”Alert”message:nildelegate:selfcancelButtonTitle:@”O(jiān)K”otherButtonTitles:nil];objc_setAssociatedObject(alert,&kRepresentedObject,interestingObject,OBJC_ASSOCIATION_RETAIN_NONATOMIC);[alertshow];
在alertView的delegate方法里面這樣操作:
12345
-(void)alertView:(UIAlertView*)alertViewclickedButtonAtIndex:(NSInteger)buttonIndex{UIButton*sender=objc_getAssociatedObject(alertView,&kRepresentedObject);self.buttonLabel.text=[[sendertitleLabel]text];}
結(jié)合以上兩者的最佳實踐
在Cocoa里面,我們經(jīng)常會見到user info這樣一個屬性,(比如NSNotification.userinfo),代表用戶自定義的payload數(shù)據(jù)。
同時一般而言,顯式的使用objc的runtime特性并不是一個良好的編程習(xí)慣,故而我們可以使用category給UIAlert添加一個user info的property,以將objc的runtime代碼進(jìn)行隱藏。
代碼與前面給出的類似,你可以在Github下載到完整Demo。傳送門
使用效果:
123
UIAlertView*alert=[[UIAlertViewalloc]initWithTitle:@"Alert One"message:@"I gonna show the userinfo"delegate:selfcancelButtonTitle:@"OK"otherButtonTitles:nil];[alertsetUserinfo:@{@"message":@"I'm userinfo of alert one"}];[alertshow];
May 22nd, 2013
使用CoreLocation來跟蹤用戶距離
背景
CoreLocation是一個強(qiáng)大的Framework,他能幫助開發(fā)使其免于復(fù)雜的位置處理而專注于應(yīng)用邏輯的開發(fā)。然而CoreLocation并沒有提供的對用戶移動距離的檢測,當(dāng)我們開發(fā)跑步類運(yùn)動類應(yīng)用時,就不可避免的需要這項功能。湊巧有一個朋友讓我?guī)兔ψ鲆粋€GPS模塊,故而就有了CYLocationManager。
代碼在Github開源托管,傳送門
實現(xiàn)說明
Readme有詳細(xì)的使用說明,我在這里主要描述一下實現(xiàn)的一些要點。
基本的思路既是不斷的采樣用戶數(shù)據(jù),過濾掉誤差較大的數(shù)據(jù),取相對誤差較小的數(shù)據(jù)進(jìn)行記錄,然后計算相鄰記錄點之間的距離。
簡單描述一下幾個要點:
當(dāng)用戶開始運(yùn)動,程序開始追蹤,設(shè)置一個強(qiáng)制標(biāo)記,(needForceCalculation),表示程序應(yīng)該忽略其他因素,立刻獲取一個點坐標(biāo)。用做起始值。
設(shè)置了CLLocationManager.headingFilter,使得程序能在用戶轉(zhuǎn)向的時候收到通知,此時設(shè)置一個強(qiáng)制標(biāo)記(needForceCalculation),使得程序在用戶轉(zhuǎn)向的時候,記錄下轉(zhuǎn)向時所在的位置,以減少誤差。
設(shè)置CLLocationManager.distanceFilter,使得程序在變化的位置大于一定數(shù)值時該更新位置才算為有效,可以避免用戶在一個地方停留,由于誤差記錄距離依然增長。
當(dāng)程序獲得位置更新時,若精度合格,切時間戳合理,則加入一個數(shù)組,用于之后的計算。若精度大于某個閥值,則認(rèn)為該位置對跟蹤距離無幫助,此時將該位置舍去。
數(shù)組currentKeepLocations來記錄最近更新的k個位置,并每隔t秒,從該數(shù)組中,取出精度最高的位置記錄。(精度見CLLocation.horizontalAccuracy)
注意,當(dāng)用戶停止運(yùn)動時,位置將無法得到更新,此時需要設(shè)置一個timer,令其在一定時間內(nèi)強(qiáng)制獲得一個位置。
該程序還可以通過每次更新位置時獲得的位置的精確度來判斷GPS信號的強(qiáng)弱。
聯(lián)系我
如果你對這個程序有疑問,請聯(lián)系我
May 16th, 2013
Copyright ? 2016 Lancy