【iOS小结】Effective Objective-C笔记

2017-11-17  本文已影响0人  WellsCai

一. 常见实用技巧

1. 在类的头文件中尽量少引用其他头文件

一般来说,应当在某个类的头文件使用向前声明来提及别的类,并在类的实现引入那些类的头文件。这样可以降低类之间的耦合。
有时无法使用声明,比如声明某个类遵守一项协议。尽量把该协议的声明放在分类中,或单独头文件引入。

2. 多用字面量语法,少用与之等价的语法

字面量语法其实是一种“语法糖”,与常规方法比更加扼要简介。

NSString *string = @"string";
NSNumber *number = @1;
NSArray *array = @[@"1",@"2",@"3"];
NSDictionary *dic = @[@"key":@"value"];
array[1];
dic[@"key"];

//前面都是不可变对象,要想创建可变对象
NSMutableArray *mutableArr = [@[@"1",@"2",@"3"] mutableCopy];
3. 多用类型常量,少用#define预处理指令

不要用预处理指定定义常量,这样定义出来的常量不含类型信息,编译器只是在编译前据此执行查找和替换操作。即使有人重新定义了常值量,编译器也不会警告。
在实现文件中使用static const 定义“只在编译单元内可见的常量”。此类常量不在全局符号表,无需加前缀。

static const NSTimeInterval kAnimationDuration = 0.5;

对外公开常量时在头文件使用extern声明全局常量,在实现文件定义其值。这种常量会出现在全局符号表,其名称要加以区隔,通常用类名作为前缀。

//.h中
extern NSString *const EOCStringConstant;
//.m中
NSString *const EOCStringConstant = @"VALUE";
4. 用枚举表示状态、选项、状态码

用NS_ENUM和NS_OPTION(用于多个选项同时使用)宏来定义枚举类型,并指明其底层数据类型。在处理枚举类型的switch语句中不要实现default分支。


宏定义.png
5. 用前缀避免命名空间冲突
Objective-C没有其他语言那种内置的命名空间机制。如果发生命名冲突,那么应用程序的链接过程就会出错。 命名冲突.png

所以要选择与你的公司、应用程序或二者皆有关联之名称作为类名(包括分类)的前缀。一个容易忽略的地方,实现文件里面的纯C函数和全局变量,在编译好的目标文件中,这些名称要作为“顶级符号”,所以也要加上前缀。

6. 总为第三方的分类名称加前缀

向第三方添加分类时,要为分类名称和里面的方法名加上自己专用的前缀。

7. 勿在分类中声明属性

把封装数据所用的全部属性都定义在主接口(主文件)。在分类拓展其他功能(包括存取方法),但尽量不要定义属性。

8. 尽量使用不可变对象

尽量使用不可变对象。若某属性仅对对象内部修改,则在.m中将其readonly属性拓展为readwrite属性。
不要把可变的collection作为属性公开(防止通过该属性直接修改内容,有可能还要执行某些操作),而应是提供相关方法来修改对象中的collection。

9. 为私有方法名加前缀

为私有方法的名称加前缀容易将其和公共方法区分开。不能单用_作为前缀,这是预留给苹果公的。

- (void)p_doSomething{
    //.........
}

二. 高级技巧部分

1. 提供全能初始化方法

在类中提供一个全能初始化方法,其他初始化方法均应调用此方法。
若全能初始化方法与父类不同,则需要覆盖父类对应的方法。如果父类的初始化方法不适合子类,那么应该覆写这个父类方法,并在其中抛出异常。

- (instancetype)init{
    @throw [NSException exceptionWithName:NSInternalInconsistencyException reason:@"Must use initWithDimension: instead " userInfo:nil];
}
2. 实现description方法

实现description方法返回一个有意义的字符串,用以描述该实例。如果想在调试时打印出更详细的对象信息(用LLDB调试),可以实现debugDescription方法。

3. 在对象内部尽量直接访问实例变量

在对象内部读取数据时,应该直接使用实例变量来读(不需要经过方法派送,直接访问内存),而写入数据时,应该通过属性来写。
在初始化方法和dealloc方法中,总是应该直接使用实例变量来读写数据。懒加载情况下,需要通过属性来读写数据。

4. 以“类族模式”隐藏实现细节

使用类族模式可以灵活应对多个类,将它们的实现细节隐藏在抽象基类中,以保持接口简介。用户无需创建子类实例,只需调用基类方法来创建即可。系统框架中有很多类族,比如NSArray和NSMutableArray。

/*   Employee.h    */
typedef NS_ENUM(NSUInteger,EmployeeType) {
    EmployeeTypeDeveloper,
    EmployeeTypeDesginer,
    EmployeeTypeFinance
};
@interface Employee : NSObject
+ (Employee)employeeWithTypt:(EmployeeType)type;
- (void)doWork;
@end

/*   Employee.m    */
@implementation Employee
+ (Employee)employeeWithTypt:(EmployeeType)type{
    switch (type) {
        case EmployeeTypeDeveloper:
            return [[EmployeeDeveloper alloc] init];
            break;
        case EmployeeTypeFinance:
            return [[EmployeeFinance alloc] init];
            break;
        case EmployeeTypeDesginer:
            return [[EmployeeDesginer alloc] init];
            break;
    }
}
- (void)doWork{
    //子类实现
}
@end


/*   EmployeeDesginer.m    */
@implementation EmployeeDesginer
- (void)doWork{
    //具体实现
}
@end
5. 在既有类中使用关联对象存放自定义数据

有时需要在某类存放相关信息,当我们不方便继承该类来改写,就可以直接在该类使用关联对象。不过使用关联对象容易引入难以查找的BUG,比如循环引用。

//设置关联对象
objc_setAssociatedObject(id  _Nonnull object, const void * _Nonnull key, id  _Nullable value, objc_AssociationPolicy policy)

//获取关联对象值
objc_getAssociatedObject(id  _Nonnull object, const void * _Nonnull key)

//移除所有关联对象值
objc_removeAssociatedObjects(id  _Nonnull object)
6. 通过委托与数据源协议进行对象间的通讯

常规的委托模式中,信息从类流向受委托者(Delegate)。也可以用协议定义一套数据源接口,让类从数据源(DataSource)获取数据,这样信息就是从数据源流向类。(比如UITableView中的delegate和DataSource,一个处理用户和列表的操作,一个提供列表显示的数据)

委托模式.png

若有必要,可实现含有位段的结构体,将委托对象能否响应协议方法缓存其中。

struct {
        unsigned numberOfSectionsInTableView : 1;
        unsigned titleForHeaderInSection : 1;
        unsigned titleForFooterInSection : 1;
        unsigned commitEditingStyle : 1;
        unsigned canEditRowAtIndexPath : 1;
    } _dataSourceHas;
7. 将类的实现代码分散到便于管理的数个分类中

使用分类机制把类的实现代码按功能划分成易于管理的小块。这样类中的方法也不会过于臃肿,使用分类也便于调试。
将私有方法归入名叫Private的分类中,以隐藏细节。

8. 使用类拓展(匿名分类)隐藏实现细节

通过类扩展向类中新增实例变量,也把私有方法的声明放在其中。
如果某属性在主接口声明readonly,而类内部又要设置方法修改此属性,那就在类拓展中将其改为readwrite。
如果想让遵守的协议不为人知,则可在类拓展中声明。

9. 通过协议提供匿名对象

协议可在某种程度上提供匿名类型。具体的对象类型可以淡化成遵守某协议的id类型,协议里规定对象所应实现的方法。如果具体类型不重要,重要的是对象能够响应(定义在协议里)的特定方法,那么可以使用匿名对象来表示。

- (void)setValue:(id<NSCopying>)value forKey:(NSString *)key
10. 在dealloc方法中只释放引用并解除监听

在dealloc方法中,应该做的事就是释放指向其他对象的引用,并取消监听(KVC或通知)。如果对象持有文件描述符或套接字等系统资源,应该在dealloc之前提供一个close方法来释放资源。
执行异步任务的方法不应在dealloc里调用,只能在正常状态下执行的那些方法也不应在dealloc里调用,因为此时对象已经处于被回收阶段。

11. 编写“异常安全代码”时留存内存管理问题

如果手动管理引用计数,而且必须捕获异常,一定要注意将try内所创立的对象清理干净。

UIView *view = nil;
@try{
    view = [[UIView alloc] init];
    [view addSubview:[UIView new]];
}
@catch(...){
    NSLog(@"there was an error")
}
@finally{
    [view release];
}

若只用ARC且要捕获异常,则需要打开编译器的-fobjc-arc-expections标志,因为ARC不自动生成安全处理异常所需的清理代码。开启标志后,编译器会自动生成这种代码,不过会导致程序变大,降低运行效率。在发现大量异常捕获操作时,应该考虑重构代码,使用NSError错误传递法来取代异常。

12. 用僵尸对象调试内存管理问题

系统在回收对象时,可以不将其真的回收,而是将它转成僵尸对象。通过环境变量NSZombieEnabled可开启此功能。
系统会修改对象的isa指针,另其指向特俗的僵尸类,从而使该对象变成僵尸对象。僵尸类能响应所有的选择子,响应方式为:打印一条包含消息内容及其接收者的消息。然后终止程序。

13.多用块枚举,少用for循环

遍历collection有四种方法。

NSArray *array = @[@"2",@"3",@"4"];
NSEnumerator *enumer = [array objectEnumerator];
id object;
while ((object = [enumer nextObject]) != nil){
     NSLog(@"%@",object);
}
//反向
for (id object in [array reverseObjectEnumerator]) {
    NSLog(@"%@",object);
}
[array enumerateObjectsUsingBlock:^(id  _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
    NSLog(@"%@",obj);
    //遍历到下标1就停止
    if (idx == 1) {
        *stop = YES;
    }
}];

块遍历法是最新、最先进的。块遍历法可以获取更多信息,也可以修改方法签名(id obj -> 确定类型),避免类型转换。另外,块遍历法本身能通过GCD并发执行遍历操作,无需另外编写代码,而其他遍历方式则无法轻易实现这一点。

16.对自定义其内存管理语义的collection使用无缝桥接

通过无缝桥接技术,可以在Foundation框架的OC对象和CoreFoundation框架的C语言结构体之间来回转换。

NSArray *array = @[@"1",@"2",@"3"];
//cfArray是指向struct__CFArray的指针
CFArrayRef cfArray = (__bridge CFArrayRef)array;
NSLog(@"count = %ld",CFArrayGetCount(cfArray));

在CoreFoundation层面创建collection时,可以指定许多回调函数来处理其元素。然后,通过无缝桥接技术,将其转换为具备特殊内存管理语义的OC对象。

14.构建缓存时选用NSCache而非NSDictionary

实现缓存时应选用NSCache而非NSDictionary。因为NSCache可以提供优雅的自动删减功能,而且是线程安全的,此外,它与字典不同,并不会拷贝键。
可以给NSCache对象设置上限,用以限制缓存中对象个数及总成本,而这些尺度则定义了删减其中对象的时机。但是绝对不能把这些尺度当成可靠的硬限制,它们仅对NSCache起指导作用。

- (void)downloadDataWithURL:(NSURL *)url{
    NSPurgeableData *cahceData = [_cache objectForKey:url];
    if (cahceData) {
        //purge引用计数 +1,
        [cahceData beginContentAccess];
        
        [self useData:cahceData];
        
        //purge引用计数 -1,变为0告诉系统必要时可以丢弃自己占据的内存
        [cahceData endContentAccess];
    }else{
        //网络下载数据
        NSData *fetchData = [self fetchDataWithURL:url];
        
        NSPurgeableData  *purgeableData = [NSPurgeableData dataWithData:fetchData];
        
        [_cache setObject:purgeableData forKey:url cost:purgeableData.length];
        
        //不需要beginContentAccess,类似于内存管理,创建的过程purge引用计数也会加1
        [self useData:cahceData];
        
        [cahceData endContentAccess];
    }
}

将NSPurgeableData和NSCache配套使用,可以实现自动清除功能。当NSPurgeableData对象所占内存为系统所丢弃时,该对象自身也会从缓存中清除。
如果缓存使用得当,那么应用程序的响应速度就会提高。只有那种重新计算起来很费事的数据,才值得放入缓存,比如那些需要从网络获取或从磁盘读取的数据。

15.精简load和ininialize的实现代码

在加载阶段,如果类实现了load方法,那么系统就会调用它。分类里也可以定义此方法,类的load方法要比分类的先调用。与其他方法不同,load方法不参与覆写机制(只会实现类自身的load方法)。
在load方法中,尽量减少执行的操作,因为整个程序在执行load方法时变得阻塞,不要在里面调用可能加锁的方法,正常也不写其他任务。其主要作用是用来调试,比如在分类写方法判断是否正确加载。
首次使用某个类之前,系统会向其发送ininialize消息(惰性加载)。由于此方法遵从覆写规则,所以通常要在里面判断初始化的是哪个类。无法在编译期设定的全局常量,可以放在ininialize初始哈。

static NSMutableArray *array;
//会先调用父类的再调用自己的
+ (void)initialize{
    if (self == [YCCache class]) {
        //执行操作
        array = [NSMutableArray array];
    }
}

所以load方法和ininialize方法应该实现得精简一点,有助于保持应用程序响应能力,也能减少引入依赖环。

16.别忘了NSTimer会保留其目标对象

NSTimer对象会保留其目标,直到计时器本身失效为止,调用invalidate方法可使计时器失效。另外,一次性的计时器在触发完任务后也会失效。反复执行的计时器容易引入保留环(比如计时器和控制器),可以扩充NSTimer的功能,用Block来打破保留环。

@implementation NSTimer (YCBlocksSupport)

- (NSTimer *)yc_scheduledTimerWithTimeInterval:(NSTimeInterval)interval block:(void (^)())block repeats:(BOOL)repeats{
    return [NSTimer scheduledTimerWithTimeInterval:interval target:self selector:@selector(yc_blockInvoke:) userInfo:block repeats:repeats];
}

- (void)yc_blockInvoke:(NSTimer *)timer{
    void (^block)() = timer.userInfo;
    if (block) {
        block();
    }
}
@end
17. 不要使用retainCount

虽然在ARC已将retainCount方法废弃了,但是即使在MRC中也是不应该调用的。对象的保留计数看似有用,实际上在任何给定的时间点的“保留计数”都无法反应对象生命周期的全貌。
比如像单例对象,其引用计数很大,也绝对不会变,其保留和释放操作都是空操作。即使是普通对象,可能也处于自动释放池中,其保留计数也不是准确的。retainCount可能永远不返回0,因为有时系统会优化对象的释放行为,在保留计数为1的时候就把它回收了。

三. 对象相关概念

1. 了解Objective-C的起源

Objective-C使用的是“消息结构”而非“函数调用”。区别在于使用消息结构的语言,其运行时所需执行的代码由运行环境决定,而使用函数调用的语言,则由编译器决定。
Objective-C为C语言添加了面向对象的特性,是其超集。Objective-C使用动态绑定的消息结构,在运行时才检查对象类型。接收一条消息之后,执行什么代码由编译环境决定。

2. 理解“属性”

使用属性@property,编译器会在编译器自动合成访问这些属性所需的方法,并且自动向类中添加适当类型的实例变量(以_开头)。如果想改名,可以使用@syntheszize语法。如果想阻止编译器自动合成存取方法,可以使用@dynamic关键字。

3. 理解“对象等同性”

想判断对象的等同性,需要重写“isEqual:”和hash方法。相同的对象必须有相同的哈希码,但是哈希码相同的对象却未必相同。编写hash方法时,应该使用计算速度最快且哈希碰撞几率低的算法。

hash方法.png

不要盲目地逐个监测每个属性,应该依照具体需求来指定监测方案。


对象等同性判断.png
4. 理解Objective-C错误模型

只有发生了可使整个应用程序崩溃的严重错误时,才使用异常。

- (instancetype)mustOverwriteMethod{
    @throw [NSException exceptionWithName:NSInternalInconsistencyException reason:@"Must be overwriden " userInfo:nil];
}

在错误不那么严重的情况下(提醒用户即可),可以用委托方法来处理错误(将NSError传给处理异常的类)。

- (void)connection:(NSURLConnection *)connection didiFailWithError:(NSError *)error;

也可以将错误信息放在NSError对象里,经由"输出参数"返回给调用者。

- (void)test{
    NSError *error = nil;
    [self doSomething:&error];
    if (error) {
        //.....
    }
}

- (void)doSomething:(NSError * __autoreleasing *)error{
    //.....
}
5. 理解NSCopying协议

若想让自己写的对象有Copy功能,则需要实现NSCopying协议。若自定义的对象有可变版本和不可变版本,就要同时实现NSCopying和NSMutableCopying协议。
复制对象时需决定采用深拷贝还是浅拷贝,一般情况下推荐使用浅拷贝。如果需要深拷贝,则考虑新增一个专门进行深拷贝的方法。

四. GCD相关技巧

1. 多用派发队列,少用同步锁

有多个线程要执行同一份代码时,为防止出错,通常要使用锁来实现某种同步机制。
第一种是采用内部的同步块。

- (void)synchronizedMethod{
    @synchronized(self){
        //....(safe)
    }
}

该实例中同步行为针对的对象使self。虽然可以保证每个对象实例都能不收干扰得运行synchronizedMethod方法,但是滥用@synchronized(self)会降低代码效率。因为共用一个同步锁的同步块,都必须按顺序执行。若是在self对象频繁加锁,那么程序可能要等另一段与此无关的代码执行完毕,才能继续执行当前代码。
另一种是直接使用NSLock对象。

_lock = [NSLock alloc] init];
- (void)synchronizedMethod{
    [lock lock];
    //....(safe)
    [lock unlock];
}

这两种方法都很好,不过也有缺陷。同步块会导致死锁,效率也不高。直接用锁对象的话,遇到死锁会很麻烦。所以可以使用GCD加锁,更加简单、高效。

_syncQueue = dispatch_queue_create("com.text.www", DISPATCH_QUEUE_SERIAL);

//设置可以不用同步,所以把同步派发改成异步派发。但是异步派发需要拷贝代码块,所以在执行代码块的任务比较繁重时才考虑这样子做
- (void)setSomething:(NSString *)something{
    dispatch_async(_syncQueue, ^{
        _something = something;
    });
}
- (NSString *)something{
    __block NSString *temp;
    dispatch_sync(_syncQueue, ^{
        temp = _something;
    });
    return temp
}
_syncQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);

- (void)setSomething:(NSString *)something{
    dispatch_barrier_async(_syncQueue, ^{
        _something = something;
    });
}
- (NSString *)something{
    __block NSString *temp;
    dispatch_sync(_syncQueue, ^{
        temp = _something;
    });
    return temp
}
栅栏块.png

将同步和异步派发结合起来,可以实现和普通加锁机制一样的同步行为,这样做不会阻塞执行异步派发的线程。使用同步队列和栅栏块,可以使同步行为更加高效。

2. 多用GCD,少用performSelector系列方法

performSelector系列方法在内存管理方法容易有疏失。它无法确定将要执行的选择子具体是什么,因此ARC编译器无法适当地插入内存管理方法。
performSelector系列方法所能处理的选择子(方法)太过局限,选择子的返回值类型(只能是id,即对象)及发送给方法的参数个数(2个)都受到限制。

- (id)performSelector:(SEL)aSelector withObject:(id)object1 withObject:(id)object2

所以应该要用对应的GCD方法来替代。

3. 掌握GCD(派发队列)和操作队列的使用时机

GCD是纯C的API,操作队列(NSOperation和NSOperationQueue)是Objective-C的对象。使用操作队列的优点有:

操作队列有很多地方胜过派发队列,提供了多种执行任务的方法。但是具体选择还要看运用场景。

4.不要使用dispatch_get_current_queue
//创建A、B两个串行队列
dispatch_queue_t queueA = dispatch_queue_create("com.test.queueA", NULL);
dispatch_queue_t queueB = dispatch_queue_create("com.test.queueB", NULL);

dispatch_sync(queueA, ^{
    dispatch_sync(queueB, ^{
        dispatch_sync(queueA, ^{
            //deadLock
        });
    });
});

这段代码执行到最内层的派发操作时,总会死锁。最里面的任务(最里层的dispatch_sync)是加到A的队列后面,所以必须最外层的dispatch_sync执行完,而最外层的dispatch_sync又必须等里面所有的任务执行完(包括最里层的dispatch_sync)。


死循环说明图.png

为了怕重入(从原来的串行队列又派发任务),可能想使用dispatch_get_current_queue判断是不是之前的队列来解决,代码如下。

dispatch_sync(queueA, ^{
    dispatch_sync(queueB, ^{
        dispatch_block_t block = ^{ /*任务*/ };
        if (dispatch_get_current_queue() == queueA) {
            block();
        }else{
            dispatch_sync(queueA, block);
        }
    });
});

实际上,这样也不能避免。因为dispatch_get_current_queue返回的是queueB而不是queueA。
队列之间会形成一套层级体系,意味着排在某个队列的块,会在其上级队列中执行。层级里地位最高的总是那个全局并发队列。dispatch_get_current_queue获取的是它当前的队列。


派发队列层级体系.png

要解决这个问题,最好的方法就是使用dispatch_get_current_queue给队列设置标识,然后判断是否是原队列。

//创建A、B两个串行队列
dispatch_queue_t queueA = dispatch_queue_create("com.test.queueA", NULL);
dispatch_queue_t queueB = dispatch_queue_create("com.test.queueB", NULL);
dispatch_set_target_queue(queueB, queueA);

static int specificKey;
CFStringRef specificValue = CFSTR("queueA");
dispatch_queue_set_specific(queueA, &specificValue, &specificKey, (dispatch_function_t)CFRelease);

dispatch_sync(queueA, ^{
    dispatch_sync(queueB, ^{
        dispatch_block_t block = ^{ /*任务*/ };
        //通过key获取标识符
        CFStringRef retrievedValue = dispatch_get_specific(&specificKey);
        if (retrievedValue) {
            block();
        }else{
            dispatch_sync(queueA, block);
        }
    });
});
上一篇下一篇

猜你喜欢

热点阅读