iOS如何写出高质量的代码笔记

96
Link913
0.5 2017.03.02 18:14* 字数 6411

前言

最近在学习这本书,Effective Objective-C 2.0 编写高质量iOS与OS X代码的52个有效方法,我会记录一些我不知道或者平时忽略的东西把它记下来.看了一部分感觉收获还是挺大的.

熟悉OC

1,了解OC

所有的OC对象都是在堆中,我们在栈中记录对象分配的地址.

只保存非对象类型的数据,我们可以使用结构体,避免了分配内存和释放内存所造成的额外开销.

2,在类的头文件中,尽量少引入其他头文件

将引入头文件的时机,尽量延后,只有在真正去使用它的时候再去引入头文件,这样可以缩短编译时间.如果一定要在头文件使用到这个类,但并不需要去管他具体的细节,我们可以使用向前声明(@class)来告诉编译器这是一个类.

有时无法使用向前声明,而我们却又需要遵循这个协议时,我们可以将它延后至延展时再去遵守.

协议最好写成一个文件,委托协议是不需要去单独写一个文件的,协议中如果需要使用了其他类的话请使用向前声明(@class)来标识,导入类的头文件请在实现文件导入.

3,多用字面量语法,少用与之等价的方法

NSString,NSNumber,NSArray,NSDictionary等创建时我们可以直接使用字面量语法来创建,如下图所示:

而不是这样子了来创建:

这样子创建可读性高,另外当我们给数组中或者字典中传入nil对象时,XCode会给我们警告,而如果使用第二种方法,当object2为nil时,那这个数组就已经创建完毕了,此时数组中就只有一个对象了.这样子的异常在平时编码中不是好确定的,如果我们用字面量去创建,异常可能就好发现的多了.

4尽量不要或者少使用宏这种预编译命令,而是用类型常量代替

我们用宏来代替字符串,常量,url都是这么做的:

然而这样子做是对宏的滥用,宏其实只是起到了替换的功能,他并没有缩短代码实际的运行时间,也没有检测机制,所以宏这种东西尽量的去少用.

我们在类的内部使用类型常量一般是这么去使用的:

常量名前方要加k,static表示该变量仅在定义此变量的编译单元中可见,注意要写在实现文件头部.

如果这个常量我们要在全局去使用,例如通知的名字:

在这里常量名前一般是需要添加类名的,避免全局使用时混淆,常量的定义一般是从右至左解读,例如上面的意思是:EOCStringConstant是一个常量,而这个常量是一个指针,指向了一个字符串对象.extern关键字表示全局符号表中将会有一个名叫EOCStringConstant的符号.

5,用枚举表示状态,选项,状态码

将int和NSIntger代表的状态全部使用枚举来代替,这样的代码可读性更高,枚举的名字应该通俗易懂.

如果将传递给某个方法的选项表示为枚举类型,而多个枚举又可以同时使用,那么将各选项值定义为2的幂,以便通过按位或操作组合起来,也称作位移枚举.

在编写枚举时切记要使用NS_ENUM和NS_OPTIONS来定义枚举,确保枚举是用开发者所选的底层数据类型实现出来的.一般位移枚举使用的是后者.

当处理枚举类型时要写switch语句时,不要实现default分支,这样加入新的枚举时,编译器会自动提示我们我们没有实现这个枚举.

对象,消息,运行期

6,理解属性

iOS开发时使用nonatomic,因为就算是用atomic也不能完全保证线程安全,还需要采用更为深层的锁定机制才可以,而且使用同步锁对性能的消耗更大.例如,一个线程连续多次读取某属性值的过程中有别的线程在同事改写该值,即便使用了同步锁,还是会读到不同的值.如果线程 A 调了 getter,与此同时线程 B 、线程 C 都调了 setter——那最后线程 A get 到的值,3种都有可能:可能是 B、C set 之前原始的值,也可能是 B set 的值,也可能是 C set 的值。同时,最终这个属性的值,可能是 B set 的值,也有可能是 C set 的值。Mac开发一般使用atomic是不会有性能个方面的考虑的.

类中对象的属性,当编译器遇到了访问成员变量的代码时,他会将其替换成偏移量,表示该变量距离存放对象的内存区域的起始地址有多远,当然并不是简单的这样子处理的,否则会出现地址错乱的问题,具体的剖析看下这篇文章,书上讲的也不是很清晰.文章链接

7,对象内部尽量直接访问实例变量

  • 读取数据时应该直接通过实例变量来读取,写入数据时则应该通过属性来写入.

直接访问实例变量的速度快;直接访问实例变量时,不会调用其设置方法,这就绕过了相关属性所定义的内存管理语义;直接访问实例变量不会触发KVO,当然要看具体情况决定;通过属性来读写可以在set&&get方法中打断点,方便调试.

  • 在初始化和dealloc方法中,全部使用实例变量.因为子类很可能覆写设置方法,所以如果使用属性可能会有问题.

8,理解"对象等同性"

  • 我们在判断对象是否是相等的时候,尽量不要用"=="这个操作符,有时判断的结果并不是我们所期望的.因为该操作比较的是两个指针本身,这里应该使用"isEqual:"这个方法,当然如果你要判断的对象是字符串,字典,数组,他们作为NSObject的子类,都实现了相应的方法,例如NSString的"isEqualToString:".这样子效率会比较高一些.

  • 相同的对象它的哈希值必定是相同的,但哈希值相同的对象未必是相同的.因为哈希算法我们并不清楚,这里作者推荐了一种算法,在编写哈希算法的时候,需要多次试验,以便减少碰撞与降低运算复杂程度之间取舍.

这种做法既能保持高效率,又能使生成的哈希值在一定长度范围内
  • 不要盲目的逐个检测每条属性,在适当的环境下使用最优解.例如判断数组相等,我们可以先去判断元素的个数,再去逐个比较;例如比较两个根据数据库创建而来的只读实例,我们可以直接对他的主键进行判断.

  • 将对象放入集合中时,就不应该再改变其中的哈希值了(改变内容),否则会造成一些我们无法想到的奇怪问题.例如:

此时就会发现set中有两个相同的元素了.

  • 这里有一个作者写的判断两个对象是否相等的例子,可以参考学习一下:

首先判断指针是否相等,再然后判断属性是否相等,若不为同一个类,则判断交由超类执行.

9,以"类族模式"隐藏实现细节

类族模式最大的好处就是用户不用关心具体是如何去实现的,保持着接口的简洁性,下面用一个例子来解释一下吧.

Person类的头文件
Person类的实现文件

我们可以看到,这边的创建对象的类方法其中只需要传入性别枚举,我们就会自动的给他创造相应的对象,并且在父类(Person)中,我们并没有对doHouseWork这个对象方法添加实质性的内容.具体的内容我们交给了Person的子类Man和Woman去分别实现.这样就可以做到在调用同一个方法时,却执行了不同的内容,算是一种比较优雅的写法,常见于系统的一些类,如:NSArray等等.

调用文件

10,在既有类中使用关联对象存放自定义数据

在继承这种添加属性不可行时我们才会使用"关联对象这种做法",当然这种行为应该尽可能少的去使用,因为调试比较困难通常可能会因为"保留环"等造成一些自己想象不到的问题,常用的地方比如在分类中动态的添加属性.

关联对象的策略类型

如果关联对象成为了属性,那么他就具备相应的语义.

关联对象的三个方法

这里需要注意的是,当在设置关联对象值时,我们通常使用静态全局变量做键.因为若想用两个键匹配到同一个值,则二者必须是完全相同的指针才可以!

11,理解objc_msgSend的作用

OC是一门动态语言,函数的调用并不是在编译期决定的,类似如下图所示:

OC向对象发送消息的函数:


第一个参数代表接收者,第二个参数代表选择子,后续参数就是函数中的那些参数,其顺序不变.选择子就是方法的名字,编译器会转换为如下函数:

objc_msgSend函数会依据接收者与选择子的类型来调用适当的方法,为了完成此操作该方法需要在接收者所属的类中搜寻其"方法列表",如果能找到就跳至其实现代码,如果找不到就沿着集成体系向上查找,最终依旧找不到的话就会执行消息转发.

12,消息转发

对象在收到一个无法解读的消息是会怎么做呢?OC有一个消息转发机制,第一步先征询接收者看能否动态地添加方法来处理这个未知的选择子,若不行则进入第二步看能不能被其他接收者处理.若依旧不行就只能走完整的转发流程了.

这三种消息转发有什么区别或者我们什么情况下去使用呢?

运行时处理消息我们使用第一步,动态方法解析,转发给另一个对象我们使用第二部,被援接收者,需要转发给多个对象时,我们使用第三步,完整的消息转发,这里,我根据书本上写了一个例子,用来实现当不实现属性的set和get方法时,如何将属性值存入字典,并从字典中读取.

实现文件:

    #import "Human.h"
    #import <objc/runtime.h>

    @interface Human ()

    @property (nonatomic, strong)NSMutableDictionary *dict;

    @end

    @implementation Human

    @dynamic name;

    - (instancetype)init{
        
        if (self = [super init]) {
            _dict = [NSMutableDictionary new];
        }
        return self;
    }

    + (BOOL)resolveInstanceMethod:(SEL)sel{

        NSString *selectorString = NSStringFromSelector(sel);
        if ([selectorString hasPrefix:@"set"]) {
            class_addMethod(self, sel, (IMP)autoDictionarySetter, "v@:@");
        } else {
            class_addMethod(self, sel, (IMP)autoDictionaryGetter, "@@:");
        }
        return YES;
    }

    - (id)forwardingTargetForSelector:(SEL)aSelector{
        return self;
    }

    //get 函数
    id autoDictionaryGetter(id self,SEL _cmd){
        Human *typedSelf = (Human*)self;
        NSMutableDictionary *humanName = typedSelf.dict;
        NSString *key = NSStringFromSelector(_cmd);
        return [humanName objectForKey:key];
    }

    //set 函数
    void autoDictionarySetter(id self, SEL _cmd, id value){
        Human *typedSelf = (Human*)self;
        NSMutableDictionary *humanName = typedSelf.dict;
        NSString *selectorString = NSStringFromSelector(_cmd);
        NSMutableString *key = [selectorString mutableCopy];
        //删除冒号
        [key deleteCharactersInRange:NSMakeRange(key.length-1, 1)];
        //删除set
        [key deleteCharactersInRange:NSMakeRange(0, 3)];
        //将第一个字母小写
        NSString *lowercaseFirstChar = [[key substringToIndex:1] lowercaseString];
        [key replaceCharactersInRange:NSMakeRange(0, 1) withString:lowercaseFirstChar];
        if (value) {
            [humanName setObject:value forKey:key];
        } else {
            [humanName removeObjectForKey:key];
        }
    }
    @end

13,用"方法调配技术"调试"黑盒方法"

书上的例子实质上就是使用运行时交换方法,在新方法中增加一些日志等功能,切记不可滥用.

14,理解类对象

类本身其实是一个结构体,结构体的第一个变量是isa指针,该变量定义了对象所属的类,结构体里面还有super_class定义了本类的超类.类对象所属的类型,也就是isa指向的类型是另外一个类,被称为元类,用来描述类对象,类方法就位于此处,每个类仅有一个类对象,这里可以变相的理解为单例,而每个类对象仅有一个与之相关的元类.假设有个someClass从NSObject中继承而来,如下图所示:

在类继承体系中查询类型信息是,使用"isMemberOfClass:"判断对象是否是某个类的实例,使用"isKindOfClass:"判断是否为某个类或其派生类的实例,之所以不使用"=="来判断,因为有的类实现了消息转发,使用"class"返回的是发起代理的对象而非接受代理的对象.

接口与API设计

15,用前缀避免命名空间冲突

  • 类名,方法名前应加上公司或项目前缀,最好为三位英文字母.
  • 若自己开发的程序库中使用了第三方库,应为其添加前缀.

16,提供"全能初始化方法"

  • 在类中提供一个全能初始化方法,并在文档中指出,其他初始化方法应该调用此方法.

    - (instancetype)initPerson{
          if (self = [super init]) {
              _name = @"Peter";
          }
          return self;
      }
    
      - (instancetype)init{
          return [self initPerson];
      }
    
  • 如果全能初始化方法不同,那么在子类中应该重写这个方法的内容.
    *如果超类的初始化方法不适用子类,我们应该在子类中重写并抛出异常!

    - (instancetype)init{
          
          @throw [NSException exceptionWithName:NSInternalInconsistencyException reason:@"必须用initPerson来初始化" userInfo:nil];
      }
    

17,实现description方法

  • 自己自定义的类实现description方法,返回一个有意义的字符串.
  • 我们调试的时候可以重写这个方法

18,尽量使用不可变对象

  • 要尽量创建不可变对象

  • 若某属性仅在外部读取,内部用来修改,我们可以在头文件将它定义为只读,在扩展里可以重新定义为可读可写.

  • 不要把可变的集合作为属性公开,而应该提供相应的方法使其修改可变集合

19,使用清晰而协调的命名方式

  • 给方法命名
  • 要点

20,为私有方法名加前缀

  • 给私有方法名前加上前缀,以便于将其和共有方法区分开,避免随意改动公共方法.

21,理解OC的错误模型

  • 只有发生了严重错误时我们才应该去使用异常

  • 在错误不严重时,我们可以用委托方法来处理错误,也可以将错误放在NSError对象里,经过输出参数传递给调用者.

22,理解NSCopying协议

  • 如果想令自己所写的对象具有拷贝功能,那么需要实现NSCopying协议,我们copy的时候需要实现copyWithZone:这个方法,实现不可变->可变的copy需要实现NSMutableCopying协议中的mutableCopyWithZone:这个方法.
  • Foundation框架中的所有集合类在默认情况下都执行浅拷贝

  • 复制对象时一般情况下尽力执行浅拷贝

  • 如果需要深拷贝.可以另外写一个深拷贝的方法:

协议与分类

23通过委托与数据源协议进行对象间通讯

  • 如果有必要,可实现含有位段的结构体,将委托协议是否能响应相关协议方法缓存到其中

      @interface MRCLoginController (){
          struct {
              unsigned int didReceiveData       :  1;
              unsigned int didFailWithError     :  1;
              unsigned int didUpdateProgressTo  :  1;
          }_delegateFlages;
      }
    

在协议的delegate的set方法中对是否实现了方法的判断进行一次缓存:

调用的时候直接从缓存判断即可

24,将类的实现代码分散到便于管理的数个分类之中

  • 使用分类机制,将类的实现代码划分为易于管理的小块.
  • 将私有的方法应该归为private分类中,可以有效地隐藏细节

25,总是为第三方类的分类名称增加前缀

  • 给第三方添加分类时总应该将类名以及方法名添加前缀,避免覆写.

26,勿在分类中声明属性

  • 把封装数据所用的属性全都放在主接口文件中
  • 在扩展之外的分类中,尽量只定义方法,不要定义属性

27,使用"class-continuation分类",隐藏实现细节

  • 通过扩展向类中增加实例变量
  • 如果某属性在外部为可读,内部又想写入,可在扩展中将属性改为可读写
  • 将私有方法声明在扩展中,但我觉得这样子做没有必要
  • 在延展中遵从协议

28,通过协议提供匿名对象

内存管理

29,理解引用计数

  • 保留和释放引用计数只能说引用计数递增或者递减
  • 属性存取方法中应该先保留旧值再释放新值

30,以ARC简化引用计数

  • ARC只负责管理OC对象的内存,尤其要注意:CoreFoundation对象和由malloc()函数分配在堆中的内存不归ARC管理,开发者需要适时调用CFRetainCFRelease.

31,在dealloc方法中只释放引用并解除监听

  • 若对象持有文件描述符等系统资源,应该编写一个方法来释放此种资源,若没有调用应该去主动提示开发者

32,编写"异常安全代码"时,留意内存安全问题

  • 捕获异常时,一定要将try块内创建的对象清理干净
  • 默认情况下,ARC不生成处理异常所需要的代码,因为在OC中,只有当应用程序因为异常而终止时此时才会抛出异常,但此时处理异常也没有太大的意义.
  • Objective-C++模式下我们应该编写对异常的处理代码,此时性能损失不大
  • 默认情况下ARC不生成安全处理异常所需要的代码.开启编译器标志后,可以生成这种代码,但会导致应用程序变大,降低运行效率

33,以弱引用避免保留环

  • unsafe_unretainedweak的区别,对象被回收以后,unsafe_unretained依然指向被释放的对象,而weak则指向了nil.

34,以"自动释放池块",降低内存峰值

  • 自动释放池排布在栈中,对象收到autorelease后,系统将其放入最顶端的池里
  • 合理运用自动释放池,能够降低应用程序的内存峰值
自动释放池会等下一次事件循环时才释放,在这里加一个可以有效避免内存峰值,看需求
  • 一般不需要去关注自动释放池这个东西

35,用"僵尸对象"调试内存管理问题

  • 系统回收对象时,可以不将其真的回收,而是转化为僵尸对象.通过Scheme中的Enable Zombie Objects选项进行配置
  • 释放时,系统会修改对象的isa指针,令其指向特殊的僵尸类,从而使该对象变为僵尸对象.僵尸可以响应所有的选择子,响应方式:打印一条包含消息内容及其接收者的信息,然后终止应用程序

36,不要使用"retainCount"

块与大中枢派发

37,理解"块"这一概念

  • 块是C,CPP.OC中的语法闭包;
  • 块能够捕获他声明范围中的对象,使用成员变量时也能捕获self,注意保留环问题
  • 块在声明的时候一般是分配在内存中的栈中的,过了使用范围后,可能会被回收,因为栈是具有内存自动回收机制的,此时我们可以将其拷贝到堆中,那么块就具有像对象那样的内存管理机制了.

38,为常用的块类型创建typedef

  • 用typedef重新定义块类型

39,用handler块降低代码

  • 在创建对象时,可以使用内联的handler块将相关业务逻辑一并声明
  • 有多个实例需要监控时,如果采用委托模式,经常需要根据传入的对象来回切换,而如果采用handler块来实现,那么可以将块与相关对象放在一起
  • 设计API时如果用到了handler块,那么可以增加一个参数,用来决定这个块在什么线程下执行

40,用块引用其所属对象时不要出现保留环

  • 在调用者使用了weakSelf时,我们也应该在合适的地方解除保留环,不能讲所有的责任都推给调用者

41,多用派发队列,少用同步锁

  • 使用同步队列及栅栏块,可以令同步行为更加高效.例如属性的读取可以设为并行,而写入则用栅栏块来实现,并发队列如果发现接下来需要执行的是一个栅栏块,那么他会等所有的并发块执行完毕以后再去执行这个栅栏块

42,多用GCD,少用performSelector系列方法

  • performSelector系列方法在内存管理方面容易有疏失,他无法确定要执行的选择子到底是什么,因而ARC编译器也就无法插入适当的内存管理方法.
  • performSelector系列方法所能处理的选择子太过局限了,选择子的返回值类型及发送给方法的参数个数都将受到限制
  • 如果想把一个任务放到另外一个线程上执行,最好使用大中枢派发机制

43,掌握GCD以及操作队列的使用时机

  • 解决多线程与任务管理问题时大中枢派发队列并非唯一方案
  • 操作队列NSOperation是一个更为重量的OC对象,他与大中枢派发相比具有以下特点:可以取消未执行的操作,指定操作间的依赖关系,可以通过KVO观察NSOperation的一些属性,指定操作的优先级

44,通过Dispatch Group机制,根据系统资源状况来执行任务

  • 一系列任务可归于一个dispatch group中,开发者可以在这组任务完成时获得通知
  • 通过dispatch group,可以在并发式派发队列中同时执行多项任务.此时GCD会根据系统资源状况来调度执行这些并发执行的任务

45,使用dispatch_once来执行只需运行一次的线程安全的代码

  • 标记应该声明在static或者global作用域中,这样的话,在把只需执行一次的块传给dispatch_once函数时,传进去的标记也是相同的

46,不使用dispatch_get_current_queue

  • 派发概念是按照层级来组织的,无法单用某个队列对象来描述"当前队列"这一概念

系统框架

47,熟悉系统框架

  • 知道有这么回事就行了,想仔细研究还应该具有C语言基础

48,多用块枚举,少用for循环

  • "块枚举法"本身就能通过GCD并发执行遍历操作,无需另行编写代码.

49,对自定义其内存管理语义的集合使用无缝桥接

  • __bridge:ARC仍具有这个OC对象的所有权
  • __bridge_retained:ARC交出对象的所有权
  • __bridge_transfer:将C数据结构转为OC对象,同时令ARC获取对象所有权
  • CoreFoundation层面创建集合时,可以指定许多回调函数,这些函数表示此集合该如何处理其元素.然后可以运用无缝桥接技术,将其转换成具备特殊管理语义的OC集合

50,构建缓存时,选用NSCache而非字典

  • NSCache可以提供优雅的自动删减功能,而且是线程安全的,与字典不同他不会拷贝键
  • 可以给NSCache设置上限,用以限制缓存中的对象的总个数及总成本,这些尺度定义了缓存删减其中对象的时机,这个上限仅仅是起到了指导作用,不要把它当作硬限制
  • NSPurgeableDataNSCache搭配使用,可以实现自动清除数据的功能,也就是说,当NSPurgeableData对象所占内存为系统所丢失时,该对象自身也会从缓存中移除

51,精简initialize与load的实现代码

  • 在加载阶段,如果类实现了load方法,那么系统就会调用它.分类里也可以定义此方法,类的load方法要比分类中的先调用,load方法不参与覆写机制.
  • 首次使用某个类之前,系统会向其发送initialize消息,此方法遵从覆写机制,所以需要在里面判断当前初始化的是哪个类.
  • loadinitialize中应该尽量少写代码
  • 无法在编译期设定的全局常量,可以放在initialize方法里初始化

52,别忘了NSTimer会保留其目标对象

  • NSTimer对象会保留其目标,直到计时器本身失效为止,调用invalidate方法可令计时器失效,另外,一次性的计数器在触发完任务之后也会失效.
  • 反复执行任务的计时器,很容易引入保留环,如果这种计时器的目标对象又保留了计时器本身,那肯定会导致保留环.
  • 可以扩充NSTimer的功能,用块来打破保留环.不过,除非NSTimer将来在公共接口提供此功能,否则必须创建分类,将相关代码实现加入其中.
iOS开发
Web note ad 1