读《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阅读 158,847评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,208评论 1 292
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,587评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,942评论 0 205
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,332评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,587评论 1 218
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,853评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,568评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,273评论 1 242
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,542评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,033评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,373评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,031评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,073评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,830评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,628评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,537评论 2 269

推荐阅读更多精彩内容