讀《Effective Objective-C 2.0 編寫高質量iOS與OS X代碼的52個有效方法》(二)

??本文包含第三章:接口與API設計第四章:協議與分類第五章:內存管理。

用前綴避免命名空間問題

??因為OC不支持命名空間,所以在定義類、方法等時應盡量加入自己的公司前綴或者名字前綴什么的,最好是三個字母的,比如XYZViewController,為什么不是兩位數呢?因為蘋果宣稱保留使用“兩字母前綴”的權利...比如UI、NS等等...

實現description方法

??為自己創建的類定義description方法非常有用,這樣在使用NSLog打印當前對象的時候,就可以自定義打印一些對象相關的信息了,like this:

// 可以包含類名、地址名、以及打印一些屬性的值等,看自己需求來擴充
- (NSString *)description {
    return [NSString stringWithFormat:@"<%@: %p, \"%@ %@\">",
            [self class], self, _name, _sex];
}


// 調用和打印的時候可以這樣用
ZHCaluVC *vc = [[ZHCaluVC alloc] init];
vc.name = @"小明";
vc.sex = @"男";
NSLog(@"%@", vc);

// 打印如下
<ZHCaluVC: 0x133d06ff0, "小明 男">

??另外如果是在控制臺用po的方式,查看某個對象的調試信息,NSObject還提供了一個debugDescription方法用于在控制臺調試用:

// 可以打印一些簡單的屬性
- (NSString *)description {
    return [NSString stringWithFormat:@"%@ %@", _name, _sex];
}

// 可以打印一些需要調試用的信息
- (NSString *)debugDescription {
    return [NSString stringWithFormat:@"<%@: %p> %@ %@", [self class], self, _name, _sex];
}


// 創建一個對象
ZHCaluVC *vc = [[ZHCaluVC alloc] init];
vc.name = @"小明";
vc.sex = @"男";

??在控制臺中直接po vc的話,可以看到調用了debugDescription方法,當然如果不重寫debugDescription,則在po的時候會直接調用description方法:


readonly的運用

??當一個類對外暴露的屬性,不希望被其他類修改的時候,應盡量設置為readonly,當然如果屬性所在類想修改這個屬性,可以在匿名內部類中將該屬性設置為readwrite(貌似Swift就沒法實現類似的效果了?):

// .h中
@interface ZHCaluVC : UIViewController
// 設置為對外readonly
@property (nonatomic, copy, readonly) NSString *name;
@end


// .m中
@interface ZHCaluVC ()
// 設置為對自身readwrite
@property (nonatomic, copy, readwrite) NSString *name;
@end

為私有方法名加前綴

??可以為一個類內部要用的私有的方法加上一個前綴,比如p_,代表private,用于跟其他對外的方法做區分:

// 私有方法
- (void)p_someMethod {
}

??然后我突發奇想,如果一個創建UI的方法,需要創建一整個頁面,如果方法過長,需要拆分的話,是不是可以用s_(sub)來表示分支的方法呢?

- (void)viewDidLoad {
    [super viewDidLoad];
    
    [self p_createUI];
}

// 總的創建UI方法
- (void)p_createUI {
    [self s_createTopUI];
    [self s_createBottomUI];
    [self s_createBottomUI];
}

// 幾個分支的創建UI的方法
- (void)s_createTopUI {
    // 創建頂部UI
}
- (void)s_createMiddleUI {
    // 創建中間UI
}
- (void)s_createBottomUI {
    // 創建底部UI
}

NSException和NSError

??NSException應該用于那種導致App崩潰的嚴重錯誤,而NSError可用于一些不嚴重的錯誤,而且NSError包含了很多錯誤相關的信息,可用于處理錯誤:

// 可以設置ErrorDomain、code、userInfo
NSError *error = [NSError errorWithDomain:NSURLErrorDomain code:404 userInfo:nil];

理解NSCopying協議

??如果一個類可以被copy,就需要實現NSCopying中的- copyWithZone方法(這也是為什么在寫單例對象的時候,需要重寫該方法):

// 假設當前類有一個屬性
@property (nonatomic, copy, readwrite) NSString *name;
// 一個實例變量
{
NSString *_sex;
}


// 需要先遵守NSCopying協議
- (id)copyWithZone:(NSZone *)zone {
    // NSZone不用管,以前內存中是分為不同的zone的,現在只有一個zone了->default zone

// 生成一個當前類的實例,并把當前對象的name屬性copy給新的實例,并返回這個實例
// 這里需要注意就是name這個屬性應該copy,因為兩個對象屬于拷貝關系
// 則name這個屬性兩個對象不應該共享,而是應該各自持有一份
ZHCaluVC *vc = [[ZHCaluVC alloc] init];
// 屬性可以直接用點語法set
vc.name = [self.name copy];
// 實例變量需要用"->"
vc->_sex = [_sex copy];
    
return vc;
}

??當前類的對象的copy方法實際上就調用了copyWithZone這個方法:



??copy和mutableCopy總結:copy的話,應該總是返回一個不可變的實例,mutableCopy應該總是返回一個可變的實例。
??關于深拷貝和淺拷貝:深拷貝就是在拷貝一個對象后,將其底層數據一并拷貝,比如上面的例子,而NSArray、NSDictionary在拷貝的時候都是淺拷貝。而深淺拷貝實際上是沒有專門的協議對一個類的深淺拷貝方法進行限制的,一般實現NSCopying中的copy方法的時候,應該設置為淺拷貝,如果想提供一個深拷貝功能,也可以自定義一個- deepCopy方法

代理模式的性能優化

??核心思想是減少對delegate進行respondsToSelector的操作次數,emmmmm,還是上代碼吧,主要就是把[delegate respondsToSelector]緩存到結構體中,存儲起來:

// .h
@protocol SomeClassDelegate <NSObject>

- (void)didDoSomethingA;
- (void)didDoSomethingB;
- (void)didDoSomethingC;

@end

@interface SomeClass: NSObject

@property (nonatomic, assign) id<SomeClassDelegate> delegate;

@end
// .m
@interface SomeClass () {
    struct data {
        // 利用“位段”來設置結構體中某個字段所占用的二進制位個數
        // 如下為1,則可以表示0和1,如果是8,則可以表示0-255
        unsigned int didDoSomethingA : 1;
        unsigned int didDoSomethingB : 1;
        unsigned int didDoSomethingC : 1;
    } _delegateFlags;
}

@end

@implementation SomeClass

// 在setDelegate方法中,緩存delegate是否已經實現了代理方法
- (void)setDelegate:(id<SomeClassDelegate>)delegate {
    _delegate = delegate;
    _delegateFlags.didDoSomethingA = [delegate respondsToSelector:@selector(didDoSomethingA)];
    _delegateFlags.didDoSomethingB = [delegate respondsToSelector:@selector(didDoSomethingB)];
    _delegateFlags.didDoSomethingC = [delegate respondsToSelector:@selector(didDoSomethingC)];
}


- (void)doSomeAction {
    // 直接使用緩存好的flag來判斷delegate是否實現了某個代理方法
    // 以此減少多次判斷respondsToSelector帶來的性能問題
    if (_delegateFlags.didDoSomethingA) {
        [self.delegate didDoSomethingA];
    }
}

@end

??雖然我覺得對性能的影響并不大...但對于那些需要周期性調用的代理方法,比如每秒刷新UI什么的,還是很有用的。

將類的實現代碼分散到便于管理的數個分類之中

??可以將一個類中的多個方法,進行歸類,然后分散到當前類的多個分類中,這樣可以方便管理不同類型的方法,比如處理UI的、處理網絡請求的、處理數據包裝的等等(當然MVC可以這樣,MVVM的話,邏輯就可以放到ViewModel中了)。一些代理的實現,也可以放到分類中去實現,方便管理,比如UITableViewDelegate、UIAlertViewDelegate等:

// 定義一個ZHCaluVC類的分類,并遵守UITableViewDelegate
@interface ZHCaluVC (TableView) <UITableViewDelegate>

@end

@implementation ZHCaluVC (TableView)

// 實現UITableViewDelegate的方法
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
}

@end

??當然我個人覺得OC的Category和Swift的Extension比,還是有些麻煩的,因為代理也是需要聲明@interface的(.h和.m中都可以有),一個類分散出多個分類,也會導致聲明很多@interface,不便于管理,而且分類在新增屬性時也比較麻煩,所以以上做法在Swift中使用Extension來做會更理想。

給分類添加屬性

??分類雖然可以通過@property添加屬性,但不會自動添加get/set方法,這樣在訪問分類中的屬性的時候會因為找不到get/set方法而導致崩潰,編譯器也告訴了我們這些信息:



??不過上面的警告信息已經很明顯的告訴了我們解決辦法,通過@dynamic+關聯對象的方式實現:

// .h
@interface SomeClass (SomeCategory)

// 在分類中聲明一個屬性
@property (nonatomic, copy) NSString *name;

@end
// .m
static const char *kNamePropertyKey = "kNamePropertyKey";

@implementation SomeClass (SomeCategory)

// 聲明自己實現get/set方法
@dynamic name;

// 通過關聯對象的方式實現get/set方法
- (NSString *)name {
    return objc_getAssociatedObject(self, kNamePropertyKey);
}

- (void)setName:(NSString *)name {
    objc_setAssociatedObject(self, kNamePropertyKey, name, OBJC_ASSOCIATION_COPY_NONATOMIC);
}

@end

??不過還是不建議在分類中新增屬性的,畢竟脫離開主接口,這些屬性存在意義可能不大。

編寫“異常安全代碼”時留意內存管理問題

??try/catch中的變量,arc是不會自動幫我們release的,也就是下面的代碼,如果在try中發生了異常,實際上arr這個變量是不會被釋放掉的,也就引發了內存泄漏,當然arc也不允許手動調用[arr release]。

 @try {
    NSArray *arr = [[NSArray alloc] init];
    arr[0];
} @catch (NSException *exception) {
} @finally {
}

??當然如果想讓arc幫我們自動釋放,需要在項目設置中的“other linker flag”中添加-fobjc-arc-exceptions,這樣在編譯時arc就會自動幫我們加入try/catch中的內存管理相關代碼,當然也會拖慢編譯速度。

關于unsafe_unretained

??簡單理解的話,__weak弱引用一個對象,當對象釋放后,指針置空,也就是nil,當調用方法的時候,不會引發崩潰,雖然unsafe_unretained也是弱引用一個對象,但對象釋放后,指針指向的還是釋放的對象的內存,當調用方法的時候,就會引發崩潰。Swift中有unowned(無主引用)的概念跟這個對應。

關于自動釋放池

??實際上自動釋放池這一塊,之前在面試的時候,就被問過一次,當時的題目是這樣的(貌似是這樣的),下面的方法,如何避免內存方面的問題:

- (void)dealString {
    for (int i=0; i<10000; i++) {
        NSString *str = [NSString stringWithFormat:@"%d", i];
        NSLog(@"%@", str);
    }
}

??當然我答上來了哈哈哈,書中也是這么寫的:

- (void)dealString {
    for (int i=0; i<10000; i++) {
        
        @autoreleasepool {
            NSString *str = [NSString stringWithFormat:@"%d", i];
            NSLog(@"%@", str);
        }
        
    }
}

??正常來說,我們聲明的10000個局部變量str,會在方法執行結束進行釋放,也就是循環結束,但如果加了自動釋放池,局部變量出了自動釋放池就釋放了,所以局部變量是一邊創建一邊釋放的,會避免局部內存高峰。

僵尸對象

??這一節干貨太多了,不好總結,參見:第35條:用“僵尸對象”調試內存管理問題。大體說下來,就是如果編輯target的scheme,打開“Enable Zombie Objects”后,在調試階段,所有對象dealloc時,都會被系統自動替換為NSZombie類,并保留當前對象的一些信息,方便調試階段定位空指針問題。

?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 230,431評論 6 544
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 99,637評論 3 429
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事?!?“怎么了?”我有些...
    開封第一講書人閱讀 178,555評論 0 383
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,900評論 1 318
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,629評論 6 412
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,976評論 1 328
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,976評論 3 448
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 43,139評論 0 290
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,686評論 1 336
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 41,411評論 3 358
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,641評論 1 374
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 39,129評論 5 364
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,820評論 3 350
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,233評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,567評論 1 295
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,362評論 3 400
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,604評論 2 380

推薦閱讀更多精彩內容