美文网首页
【iOS小结】Effective Objective-C笔记

【iOS小结】Effective Objective-C笔记

作者: WellsCai | 来源:发表于2017-11-17 10:15 被阅读0次

    一. 常见实用技巧

    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有四种方法。

    • for循环
    • NSEnumerator遍历法
    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的对象。使用操作队列的优点有:

    • 取消某个操作。(GCD添加到队列中不能取消了,NSOperation在运行任务前可以cancel,不过已经启动的任务也取消不了。)
    • 指定操作间的依赖关系。
    • 通过键值观察监控NSOperation的属性,用比GCD更精细的方式来监听到任务状态的改变。(比如监听isCancelled或isFinished)
    • 指定操作的优先级。(GCD的优先级是针对队列来说的。NSOperation的线程优先级决定了运行此操作的线程处于何种优先级上。)
    • 重用NSOperation对象。

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

    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);
            }
        });
    });
    

    相关文章

      网友评论

          本文标题:【iOS小结】Effective Objective-C笔记

          本文链接:https://www.haomeiwen.com/subject/nibpmxtx.html