iOS编写高质量代码

标签: ios objective-c | 发表时间:2015-09-01 14:24 | 作者:koknine
出处:http://segmentfault.com/blogs

前言

github: https://github.com/koknine

这是一篇读书笔记,快速记录各种高效率编程的技巧和方法。这些方法是为了提升编码质量和效率,高质量代码利于后期的维护和更新,毕竟不能一份代码到永远。

由于是记录形式,当然不能把整篇内容都写下来,只记录关键性的内容,长期更新。

正文

Objective-C使用了消息机制代替调用方法。

区别:使用消息结构的语言,其运行时缩影执行的代码由运行环境来决定。而使用函数调用的语言,则又编译器决定。

头文件中少引用其他文件

在头文件中使用 @Class代替直接引用其他头文件

多使用字面量语法

      NSNumber *intNumber = @1;
    NSNumber *floatNumber = @2.5f;
    NSNumber *doubleNumber = @3.1415926;
    NSNumber *boolNumber = @YES;
    NSNumber *charNumber = @'a';
    
    int a = 3;
    float b = 2.1;
    NSNumber *c = @(a*b);
    
    NSArray *animals = @[@"cat",@"dog",@"monkey"];
    
    NSString *dog = animals[1];

    NSDictionary *dataDict = @{ @"firstName" : @"aa",
                                @"lastName" : @"bb",
                                @"age" : @20 };
    
    NSString *lastName = dataDict[@"lastName"];
    
    NSMutableArray *mutableArray = animals.mutableCopy;

多用类型常量,少用 #define预处理

如果只在本类使用的常量,使用 static const关键字来定义常量。

如果多个类都需使用到某一常量,则需将常量定义成公开的,具体方式是在类的声明文件中使用 extern const关键字声明常量,在类的实现文件中使用 const关键字定义常量,这样任何类只要导入了声明常量的头文件就可以直接使用定义好的常量了。

.h文件中声明

  extern NSString *const XFExternalConst;

.m文件中描述

  NSString *const XFExternalConst = @"ko";

为避免冲突,一般都用类名做前缀。

用枚举表示状态、选项、状态码

枚举只是一种常量命名方式,某个对象所经历的各种状态可以定义为一个枚举集。

编译器会为枚举分配一个独有的编号,从0开始每个递增加1.实现枚举所用的数据类型取决于编译器,不过其二进制位的个数必须能完全表示枚举编号才行。

  enum ConnectionState {
    ConnectionStateDisconnected,
    ConnectionStateConnecting,
    ConnectionStateConnected
};

typedef enum ConnectionState ConnectingState;

还可以不使用编译器所分配的编号,手工指定某个枚举成员所对应的值。

还有一种情况应该使用枚举类型,那就是定义选项的时候。若这些选项可以彼此组合,则更应该如此。只要枚举定义的对,各选项之间就可以通过“按位或操作符”来组合。

凡是需要以按位或操作来组合的枚举都应该用 NS_OPTIONS宏,如果没有组合需求,就用 NS_ENUM宏。

  typedef NS_ENUM(NSInteger, UIViewAnimationTransition) {
    UIViewAnimationTransitionNone,
    UIViewAnimationTransitionFlipFromLeft,
    UIViewAnimationTransitionFlipFromRight,
    UIViewAnimationTransitionCurlUp,
    UIViewAnimationTransitionCurlDown,
};

typedef NS_OPTIONS(NSUInteger, UIViewAutoresizing) {
    UIViewAutoresizingNone                 = 0,
    UIViewAutoresizingFlexibleLeftMargin   = 1 << 0,
    UIViewAutoresizingFlexibleWidth        = 1 << 1,
    UIViewAutoresizingFlexibleRightMargin  = 1 << 2,
    UIViewAutoresizingFlexibleTopMargin    = 1 << 3,
    UIViewAutoresizingFlexibleHeight       = 1 << 4,
    UIViewAutoresizingFlexibleBottomMargin = 1 << 5
};

枚举在 switch语句里面的时候,不需要加 default分支。

属性的概念

基本方法就不描述了。

@dynamic关键字,表示不要自动创建实现属性所有的实例变量,也不要为其创建存取方法。即使编译器没有发现定义存取方法,也不会报错,它相信这些方法能在运行期找到。

属性的四种特质

  • 原子性

默认情况下,编译器合成的方法会锁定机制保持 atomic。如果使用 nonatomic,则不使用同步锁。

  • 读写权限

readwrite的属性具有 gettersetter方法

readonly的属性仅具有 getter方法

  • 内存管理语义

assign只针对“纯量类型”,比如 CGFloat或者 NSInteger

strong表示该属性定义了一种 拥有关系。为这种属性设置新值时,设置方法会先保留新值,并释放旧值,然后将新值设置上去

weak表示该属性定义另一种 非拥有关系。为这种属性设置新值时,设置方法既不保留新值,也不释放旧值。和 assign类似,然而在属性所指的对象遭到摧毁时,属性值也会清空

unsafe_unretained这个和 assign相同,但是它适用于 对象类型,该特质表达一种 非拥有关系,当目标对象遭到摧毁时,属性值不会自动清空,是不安全的

copy表达的所属关系和 strong类型。然后设置方法并不保留新值,而是将其拷贝。当属性类型为 NSString *时,经常用此特质来保护其封装性,因为传递给设置方法的新值可能指向一个 NSMutableString类的实例。如果不是拷贝的花,那么设置完属性以后,字符串的值可能会在对象不知情的情况下遭人更改。所以这个时候需要拷贝一份不可变的字符串。

  • 方法名

getter=<name> 指定 getter的方法名。如果属性是 Boolean型,在方法名加上 is前缀,就可以用这个方法来指定。

setter=<name> 指定 setter的方法名。这个不常见。

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

懒加载是重写 getter方法

理解 对象等同性的概念

按照 ==操作符比较出来的结果未必是我们想要的,因为该操作符比较出来的是两个指针本身,而不是指针所指的对象。应该是用 NSObject协议中声明的 isEqual方法来判断两个对象的等同性。来办来说两个类型不同的对象总是不相等的。

  NSString *oneStr = @"aaa 21";
NSString *twoStr = [NSString stringWithFormat:@"aaa %d",21];
BOOL equalA = (oneStr == twoStr);//NO
BOOL equalB = [oneStr isEqual:twoStr];//YES
BOOL equalC = [oneStr isEqualToString:twoStr];//YES

两个用于判断等同性的关键方法

  • (BOOL)isEqual:(id)object;
    @property (readonly) NSUInteger hash;

默认实现是:当且仅当其指针值完全相等时,这两个对象才相等。

几个要点

  • 若想监测对象的等同性,提供 isEqual:与 hash 方法

  • 相同的对象必须具有相同的哈希码,但是两个哈希码相同的对象却未必相同

  • 不要盲目逐个检测每条属性,而是应该依照具体需求来制定监测方案

  • 编写 hash 方法时,应该是用计算速度快而且碰撞低的算法

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

核心套路就是类似 UIButton,创建的时候传入一个枚举值,根据枚举值来创建子类。(这里的笔记是我看懂以后写的,不知道的朋友先搜索一下 工厂模式,其实就是那个意思)

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

有时候需要在对象中存放相关信息,这时候我们通常会从对象所属的类中继承一个子类,然后改用这个子类对象。然而并非所有情况下都能这么做,有时候类的实例可能由某种机制创建。 Objective-C有一种强大机制叫 关联对象

这种机制要小心使用,因为会使代码失控。

理解 objc_msgSend的作用

原型

  void objc_msgSend(id self, SEL cmd, ...)

一个例子

  id returnValue = [someObject messageName:parameter];

编译器会把它转换为以下函数

  id returnValue = objc_msgSend(someObject,@selector(messageName:),parameter);

为了完成调用方法,该方法需要在接受者所属的类中搜寻其 方法列表,如果能找到,就跳转过去。如果找不到,就沿着继承体系向上继续查找,等找到合适的再跳转。如果最终还是找不到,就执行 消息转发的操作。

一些 边界情况,则交由另一些函数处理

  • objc_msgSend_stret 如果待发送的消息要返回结构体,可交此函数处理。

  • objc_msgSend_fpret 如果消息返回的是浮点数,可交由此函数处理。

  • ojbc_msgSendSuper 如果要给超类发送消息,例如 [super message:parameter],那么就就交由此函数处理。

理解消息转发机制

消息转发分为两大阶段。

第一阶段先问接受者,所属的类,看其是否能动态添加方法,以处理当前这个 unknown selector,这称为 dynamic method resolution

第二阶段涉及 full forwarding mechanism。如果运行期系统已经把第一阶段执行完了,那么接受者自己就无法再以动态新增方法的手段来响应包含该 selector的消息了。此时,运行期系统会请求接受者以其他手段来处理与消息相关的方法调用。然后又分两部。

首先,让接受者看看有没有其他对象能处理这条消息。如果有,就转发给那个对象。

如果没有,就会启动完整的消息转发机制,运行期系统会把消息有关的全部细节封装到 NSInvocation对象中,再给接受者最后一次机会,让它设法解决当前还未处理的这条消息。

动态方法解析

对象收到无法解读的消息后,先调用

  + (BOOL)resolveInstanceMethod:(SEL)sel 

该方法的参数就是那个未知的 selector,返回 Boolean类型,表示这个类是否能新增一个实例方法用来处理这个 selector。在继续走下去之前,这有个机会新增一个处理的方法。

如果尚未实现的不是实例方法而是类方法,则调用

  + (BOOL)resolveClassMethod:(SEL)sel

使用他们的前提是,相关方法的实现代码已经写好,只等着运行的时候动态插入在类里面。

这个常常用来实现 @dynamic属性。

后备接收者

当前接收者还有第二次机会处理,能不能把消息转发给其他接收者

  - (id)forwardingTargetForSelector:(SEL)aSelector

找得到就返回对象,找不到就返回 nil

完整的消息转发

  - (void)forwardInvocation:(NSInvocation *)anInvocation

先创建 NSInvocation对象,把尚未处理的那条消息有关的全部细节都封在其中。此对象包含 selectortarget以及参数。

继承体系中的每个类都有机会处理此调用请求,直到 NSObject。如果还没有找到,那么该方法还会继续调用 doesNotRecognizeSelector:抛出异常,此异常表示最终未能处理。

这个机制属于底层机制,可以动态注入方法,甚至之前的可以动态注入属性,云后端服务商可以说基本就靠这个套路,通过KVC的样子往类里面添加属性。

用方法调配技术调试黑盒方法

黑科技。

IMP指针,改方法实现,替换系统方法,可以多添加日志打印。

类对象

OC 是一门极其动态的语言。

每个 OC 对象实例都是指向某块内存数据的指针。

  typedef struct objc_object {
    Class isa;
} *id;

每个对象结构体的首个成员是 Class类的变量。该变量定义了对象所属的类,通常称为 is a指针。

  typedef struct objc_class *Class;
struct objc_class {
    Class isa;
    Class super_class;
    const char *name;
    long version;
    long info;
    long instance_size;
    struct objc_ivar_list *ivars;
    struct objc_method_list **methodLists;
    struct objc_cache *cache;
    struct objc_protocol_list *protocols;
};

此结构体存放类的 元数据,例如类的实例实现了几个方法,具备多少个实例变量等信息。
首个变量是 isa指针,说明 Class本身也是 OC 对象。

super_class定义了本类的超类。类对象所属的类型是另一个类,叫做 超类

每个类仅有一个 类对象,而每个 类对象仅有一个与之相关的 元类

class方法所返回的类表示发起代理的对象,而非接受代理的对象。

用前缀避免命名空间冲突

开发者可能会忽视另外一个容易引发命名冲突的地方,那就是类的实现文件中所用的纯 C 函数及全局变量。

提供全能初始化方法

所有对象均要初始化。

提供一个全能初始化方法,其他的几种初始化方法调用它。

如果全能初始化方法与超类不同,则需覆写超类中的对应方法。

实现 description方法

重写 - (NSString *)description

控制台 - (NSString *)debugDescription

尽量使用不可变对象

尽量把对外公布出来的属性设为只读,只在必要时候对外公布。

有时候想修改封装在对象内部的数据,但是却不想让外人所改动。这种情况需要将 readonly.m文件中重新生成 readwrite。但是为了避免产生意外,需要在必要时通过 dispatch queue来实现。

不要把可变的内容作为属性公开,而是提供相关方法,以此修改对象中的可变内容。

使用清洗而协调的命名方式

驼峰命名法

方法与变量以 小写字母开头

类名以 大写字母开头

不要使用 str这种简称,而用 string这样的全称

Boolean属性应该加 is前缀,如果某方法返回非属性的 Boolean值,应该根据功能选用 has或者 is当前缀

类与协议的命名

为类与协议的名称加上前缀,以避免命名空间的冲突

委托一般使用委托的发起方名称后面跟一个 Delegate

为私有方法名加前缀

一般可以使用 p_作为前缀,表示私有方法

不要用一个单独的下划线作为私有方法的前缀

理解 Objective-C错误模型

异常 NSException应该用于极其严重的错误,比如编写了某个抽象基类,它的正确用法是先从重继承一个子类,然后再使用这个子类。在这种情况下,如果有人直接使用了这个抽象基类,那么可以考虑抛出异常。

NSError的用法很灵活,封装了三条信息

  • Error domain 错误范围,类型为字符串
    错误发生的范围,通常用一个特有的全局变量来定义。

  • Error code 错误码,类型为证书
    独有的错误代码。这种错误通常采用 enum来定义,比如 HTTP 请求返回的状态码。

  • User info 用户信息,类型为字典
    有关此错误的额外信息,其中或许包含一段 本地化描述,或许还包含导致该错误发生的另外一个错误,经由此种信息,可将相关错误传承一条 chain of errors

理解 NSCopying协议

使用对象时经常需要拷贝它。如果想令自己的类支持拷贝操作

  - (id)copyWithZone:(NSZone *)zone;

为什么会出现 NSZone,以前开发的时候,会把内存分成不同的 zone,而对象会创建在某个区里面。现在不用了,每个程序只有一个 default zone

另外一个 NSMutableCopying协议,返回可变的副本

  - (id)mutableCopyWithZone:(NSZone *)zone;

深拷贝
在拷贝对象自身时,将底层数据也一并复制过去

浅拷贝
Foundation框架中所有的容器类默认情况下执行浅拷贝,只拷贝对象本身,不复制数据
因为不是所有对象都能拷贝,而且调用者也未必需要都一一拷贝。

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

委托属性要定义成 weak,因为两者之间必须为 非拥有关系

  - (BOOL)respondsToSelector:(SEL)aSelector;

也可以用协议定义一套接口,令某类从该接口获取所需的数据。委托模式的这种用法是向类提供数据,所以成为 dataSource。在这种模式中,信息从数据源流向类。而在常规的代理模式中,信息则从类流向受委托者。

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

把一个类中的几个不同模块方法写到别的文件中,合理使用 category

不要在分类中声明属性

除了 extension外,其他的分类都无法向类中新增实例变量

声明为 @dynamic,然后动态添加

使用 extension隐藏实现细节

通过协议提供匿名对象

使用匿名对象来隐藏类型名称

理解引用计数

retain 增计数
release 减计数
autorelease 待稍后清理 autorelease pool时,再减少计数

对象创建出来时,其保留计数至少为1

自动释放池

循环引用

以ARC简化引用计数

若方法名以下列词语开头,则返回的对象归调用者所有

  • alloc

  • new

  • copy

  • mutableCopy

在应用程序中,可用下列修饰符来改变局部变量与实例变量的语义

__strong 默认语义,保留这个值

__unsafe_unretained 不保留这个值,这么做可能不安全,因为等到再次使用变量时,其对象可能已经回收了

__weak 不保留这个值,但是变量可以安全使用,因为如果系统把这个对象回收了,那么变量也会自动清空

__autoreleasing 把对象 按引用传递给方法时,使用这个特殊的修饰符,此值在方法返回时自动释放

比如,想令实例变量的语义与不使用 ARC 时相同,可以使用 __weak__unsafe_unretained修饰符

block 块会自动保留其所捕获的全部对象,而如果这其中有某个对象又保留了块本身,那么就可能导致循环引用,可以用 __weak局部变量来打破这种循环引用

注意: CoreFoundation对象不归 ARC 管理,开发者必须适时调用 CFRetain/CFRelease

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

把原来配置过的观测行为都清除掉,如果使用 NSNotificationCenter给此对象注册过某种通知,那么一般应该在这里注销

(未完待续)

总结

纯属个人笔记,特别是底层机制很有作用,如今 iOS开发不再仅仅是把一个内容展现出来,里面还有涉及到各种安全性能,了解根本才是持续发展之道。

相关 [ios 质量 代码] 推荐:

iOS编写高质量代码

- - SegmentFault 最新的文章
这是一篇读书笔记,快速记录各种高效率编程的技巧和方法. 这些方法是为了提升编码质量和效率,高质量代码利于后期的维护和更新,毕竟不能一份代码到永远. 由于是记录形式,当然不能把整篇内容都写下来,只记录关键性的内容,长期更新. Objective-C使用了消息机制代替调用方法. 区别:使用消息结构的语言,其运行时缩影执行的代码由运行环境来决定.

ios代码开源

- - CSDN博客移动开发推荐文章
本人从10年开始搞ios开发,从菜鸟到现在的入门,期间遇到了许多困难,也总结了一些东西,本着开源精神,希望大家共同成长的目的把这个工程开源出来.. 这个工程是从11年到13年之前完成的.主要是我平时用到的一些基础功能模块.其中有其他开源的代码和我自己写的一些.代码结构基本乱,12年以后的代码结构还可以,不是很乱,之前水平有限,如果不怎么样就别喷我了.

IOS代码实现Hello World

- - CSDN博客推荐文章
前面写的IOS笔记一直都是用Xib文件实现的小Demo开发,但是问了好几个现在正从事IOS开发的朋友,在实际开发,并不是所有的项目都会用Xib来实现的,因为IOS以前的版本不能正常运行,因为还在学习阶段,也没有在真机上测试,所以没法验证. 但还是决定要用代码来实现Demo,也可以重新巩固一下先前学习的内容.

使用 JSLint 保证 JavaScript 代码质量

- zhibin - IBM developerWorks 中国 : Web development : Articles,Tutorials
随着富 Web 前端应用的出现,开发人员不得不重新审视并重视 JavaScript 语言的能力和使用,抛弃过去那种只靠“复制 / 粘贴”常用脚本完成简单前端任务的模式. JavaScript 语言本身是一种弱类型脚本语言,具有相对于 C++ 或 Java 语言更为松散的限制,一切以函数为中心的函数式编程思想也为开发人员提供了更加灵活的语法实现.

编写高质量的Java代码

- - 研发管理 - ITeye博客
Java 开发通用方法和准则. 不要在变量和常量出现易混淆字母:int i=1l;. 三元操作符的值类型务必保持一致;. 避免带有变长参数的方法重载:Java 5 引入了变长参数,varags, 用…表示,变长参数必须是最后一个参数,一个方法不能有多个变长参数;. 别让null和空值影响到变长方法,主要出现在在变长方法的重载;.

[技巧] 如何在 iOS 版 Safari 中“查看源代码”

- - 果迷网
需要在 iPad、iPhone 上查看网页的源代码. 很不幸,iOS 中的 Safari 没有内置这个功能. 你需要使用 下边的方法,之后就可以在 iOS 上查看任意网站的源代码了. 在 iPad、iPhone 或 iPod touch 的 Safari 中将这个页面(或者任何页面)存为书签,并命名为“查看源代码”.

如何提高代码质量(管理篇):代码复查

- - 博客 - 伯乐在线
也许你是一位项目经理,也许你是一位项目骨干成员,或者开发小组长. 在我发表“如何提高代码质量”的这一系统文章后,有许多网友都向我抱怨,说他无法把握整个项目组成员的代码质量. 我想,这也是所有项目组普遍存在的问题吧,它通常表现为以下几个问题:. 任何项目组成员都不可避免地出现新手,他们往往是刚刚从大学毕业的学生.

Google Inbox如何做到在Android、iOS和Web上共享70%的代码

- - Solidot
开发一个移动应用如果你想要面向尽可能广的用户,你需要为Android开发一个,iOS开发一个,Web开发一个. 为每个平台开发一个版本意味着三倍的工作量,三倍的bug处理量. 令整个事件变得更复杂的是,三个平台必须使用不同的语言开发:iOS是Objective-C/Swift、Android是 Java,而Web是JavaScript/CSS/HTML5.

如何编写高质量的Javascript代码

- jessie - 蓝色理想
优秀的Stoyan Stefanov在他的新书中(《Javascript Patterns》)介绍了很多编写高质量代码的技巧,比如避免使用全局变量,使用单一的var关键字,循环式预存长度等等. 这篇文章不仅仅从代码本身来考虑如何优化编码,也从代码的设计阶段来考虑,包括书写API文档,同事的review,使用JSLint.