ObjectC 上手

本文结构

参考孟岩老师的文章,对本文结构如下划分

  • 基本数据类型
  • 基本语法
  • 数组和其他集合类
  • 基本输入输出和文件处理,输入输出流类的组织
  • 序列化和反序列化
  • 面向对象特性
  • 异常、错误处理、断言、日志和调试支持,对单元测试的支持
  • RunTime
  • callback方法调用,事件驱动编程模型

参考链接

在完成本文过程中,或转载,或参考了以下链接

  • 匿名函数

http://blog.devtang.com/2013/07/28/a-look-inside-blocks/
http://coolshell.cn/articles/8309.html
http://www.jianshu.com/p/29d70274374b

  • 集合

http://blog.csdn.net/whoten/article/details/17892673
http://blog.sunnyxx.com/2014/04/30/ios_iterator/

  • 属性

http://www.devtalking.com/articles/you-should-to-know-property/
http://www.jianshu.com/p/2a9c98a29685

  • 断言&错误&日志

http://blog.csdn.net/lcl130/article/details/41889185
http://www.jianshu.com/p/6e444981ab45

  • 面向对象

http://ios.jobbole.com/83082/

  • 内存

http://blog.devtang.com/2016/07/30/ios-memory-management/

  • 单元测试

https://hjgitbook.gitbooks.io/ios/content/01-thinking/01-the-basic-knowledge-of-unit-test.html
http://www.jianshu.com/p/8bbec078cabe
http://www.cocoachina.com/ios/20150702/12253.html

  • 类别

https://tech.meituan.com/DiveIntoCategory.html

  • 回调

http://blog.csdn.net/wzzvictory/article/details/9295317
http://www.cnblogs.com/TsengYuen/archive/2011/04/20/2022060.html
http://www.jianshu.com/p/376ba5343097
https://segmentfault.com/q/1010000000387240
http://wdxtub.com/2016/02/20/dive-in-objc-1/

  • 文件&流

http://www.jianshu.com/p/fbb997eb032d
http://blog.csdn.net/swingpyzf/article/details/16325923

  • 运行时

http://www.jianshu.com/p/f73ea068efd2
http://yulingtianxia.com/page/8/

  • 其他

http://blog.csdn.net/myan/article/details/3144661
https://developer.apple.com/library/content/documentation/Cocoa/Conceptual/ProgrammingWithObjectiveC/Introduction/Introduction.html
http://www.jianshu.com/p/8b76814b3663
https://wiki.haskell.org/Cn/Introduction#Quicksort_in_Haskell
https://github.com/oa414/objc-zen-book-cn#%E9%9D%A2%E5%90%91%E5%88%87%E9%9D%A2%E7%BC%96%E7%A8%8B

基本数据类型

  • C的基本数据类型
    objC作为C语言的一个超集,所有C语言支持的基本数据类型,ObjC同样支持

  • int 与 NSInteger

    NSInteger的定义如下:

    #if __LP64__ || (TARGET_OS_EMBEDDED && !TARGET_OS_IPHONE) || T  ARGET_OS_WIN32 || NS_BUILD_32_LIKE_64
    typedef long NSInteger;
    typedef unsigned long NSUInteger;
    #else
    typedef int NSInteger;
    typedef unsigned int NSUInteger;
    #endif
    
    

    很明显,做了32位64位机器的移植性处理。

  • BOOL

    YES 或 NO

  • float 与 CGFloat

    CGFloat对于float相当于NSInteger对于int

  • NSString

    后面单独介绍。

    需要注意的是:NSString类型的等号赋值做的是深拷贝

  • NSValue

    NSValue是个可以和各种基本数据类型相互转换的类。

    [NSValue valueWithCGSize:CGSizeMake(100, 100)];
    [NSValue valueWithRange:NSMakeRange(0, 10)];
    
  • NSNumber

    NSNumber与上面不同的是,NSNumber不是基本的数据类型,而是对象。
    继承关系:

    NSNumber->NSValue->NSObject

    同时NSNumber支持和NSString一样的@符号简写

    NSNumber * number = @(123);
    NSNumber * number1 = @(3.1415);
    NSNumber * number2 = @(YES);
    
    NSInteger intValue = [number integerValue];
    CGFloat floatValue = [number1 doubleValue];
    BOOL boolValue = [number2 boolValue];
    
    

基本语法

  • 减号和加号

    1. 减号表示一个函数或消息的开始

      举个例子,在c#中,一个方法的写法是

      private void add(bool isAdd){
          ...
      }
      

      用oc写出来就是

      -(void)add:(Bool)isAdd{
          ...
      }
      
    2. 加号代表是类的静态方法,不需要实例化即可调用。

  • 中括号

    中括号可以理解为调用方法,在oc中,严格来说,应该表述为发消息

    具体理解如下:

    因为在Objective-C中,message与方法是在执行阶段绑定的,而不是编译阶段。简单的说 [a someFunc] 这样一个调用,在编译阶段,编译器并不知道someFunc要执行哪段代码。这个时候[a someFunc]会被转换为 objc_msgSend(a, "someFunc"),字面的意思也很容易理解,就是给a这个instance,发“someFunc”这个消息,以selector的形式。在运行阶段,执行到上述的objc_msgSend这个函数时。函数内部会到a对应的内存地址,寻找someFunc这个方法的地址,并执行。如果找不到,就会抛一个“unknown selector sent to instance”的异常。(比如.h中声明了方法,但.m中没有实现,就可以重现这个错误) 所以严格意义上来将,任何Objective C的函数调用,编译阶段的表现,都只能算一种“发消息”的行为。

    深入理解可见:Objective-C 消息发送与转发机制原理

    总之,从语法层面上来说。

    在C#中,我们这样写:

    this.hello(true);
    

    在oc中,我们这样写:

    [self hello:YES];
    
  • #import @interface

    1. #import 和 #include

      #import可以认为是#include的升级版,使用#import可以保证头文件不会重复引用。

      在c中,防止头文件的重复引用,常常可见类似如下代码

      #ifndef xxx_H
      #define xxx_H
      #include "xxx.h"
      #endif
      

      通常建议是:oc文件使用#import形式,c\c++文件使用#include形式。

      详见

    2. @interface 和 @implementation

      用一个简单的例子理解,定义一个老鹰捉小鸡类

      使用c#

      public class Chicken : System{
          private string ckName = "chick";
          private int ckSize = 15;
          
          private bool IsCaught(){
              return true;
          }
      }
      

      使用oc

      • Chicken.h
      @interface Chicken : NSObject{
          NSString *ckName;
          int ckSize;
      }
      
      -(BOOL)IsCaught:;
      @end
      
      • Chicken.mm
      #import "Chicken.h"
      @implementation Chicken
      
      -(void)init{
          ckName=@"chick";
          ckSize=15;
      }
      
      -(BOOL) IsCaught:{
          return YES;
      }
      
      @end
      
  • 参数格式以及参数的传递

    1. 多个参数的写法

      (方法的数据类型)方法名:(参数1数据类型)参数1数值名 参数2名:(参数2数据类型)参数2数值名 参数3名:(参数3数据类型)参数3数值名 ...

    2. 参数传递

      举个例子

      [[[MyClass alloc] init:[foo bar]] autorelease] 
      

      对应于

      MyClass.alloc().init(foo.bar()).autorelease()
      

数组和其他集合类

  • Foundation framework中用于收集cocoa对象(NSObject对象)的三种集合

  • NSArray 用于对象有序集合(数组)

  • NSSet 用于对象无序集合 (集合)

  • NSDictionary用于键值映射(字典)

以上三种集合类是不可变的(一旦初始化后,就不能改变)

  • 对应的可变集合类(这三种可变集合类是对应上面三种集合类的子类):

  • NSMutableArray

  • NSMutableSet 可修改的集合。主要用于集合运算(并集,交集,差集)

  • NSMutableDictionary 允许用户添加和删除key和value

这些集合类只能容纳cocoa对象(NSOjbect对象),如果想保存一些原始的C数据(例如,int, float, double, BOOL等),则需要将这些原始的C数据封装成NSNumber类型进行存储。NSNumber对象是cocoa对象,可以被保存在集合类中。

  • 遍历

  • 索引

    NSArray *array = [NSArray arraywithobjects:@"1",@"2",@"3",@"4",nil];  
    

NSUInteger count = [array count];
for (int i = 0 ; i ! = count;i++){
id obj = [array objectAtIndex:i];
//自定义code...
}

 
 - 迭代器
 

NSEnumerator *enumerator = [array objectEnumerator];
id obj = nil;
while(obj = [enumerator nextobject]){
//自定义code
}


- 快速枚举

for(id obj in array){
//自定义code
}

***字典使用快速枚举时,得到的obj是key而不是keypair***

- 代码块

 为什么使用代码块,因为代码块可以让循环操作并发执行。而上面的三种方式都是线性操作。
 
 ```
 [array enumerateObjectsUsingBlock:^(id obj,NSUInteger idx,BOOL *stop){  
 //obj为取出的对象,idx为对应的下标  
}];     
 ```
 
 ```
 if(idx == 1){  
*stop = YES;  
}  
 ```
 
 - 技巧

     1. 倒序遍历

     NSArray和NSOrderedSet都支持使用reverseObjectEnumerator倒序遍历,如:
     
     ```
     NSArray *strings = @[@"1", @"2", @"3"];
for (NSString *string in [strings reverseObjectEnumerator]) {
 NSLog(@"%@", string);
}
     ```
     这个方法只在循环第一次被调用,所以也不必担心循环每次计算的问题。

     同时,使用enumerateObjectsWithOptions:NSEnumerationReverse也可以实现倒序遍历:

     ```
     [array enumerateObjectsWithOptions:NSEnumerationReverse usingBlock:^(Sark *sark, NSUInteger idx, BOOL *stop) {
 [sark doSomething];
}];

     ```
     
     2.使用block同时遍历字典key,value
     
     block版本的字典遍历可以同时取key和value(forin只能取key再手动取value),如:
     
     ```
     NSDictionary *dict = @{@"a": @"1", @"b": @"2"};
 [dict enumerateKeysAndObjectsUsingBlock:^(id key, id obj, BOOL  *stop) {
 NSLog(@"key: %@, value: %@", key, obj);
 }];[]() 
     ```


## 基本输入输出和文件处理,输入输出流类的组织

- 输入输出

 兼容C的scanf,printf,不再叙述。

- 文件处理

 - 基本读写操作

     Objective-C使用NSFileHandle类对文件进行基本操作,iOS文件操作

     NSFileHandle类中得方法可以对文件进行基本的读写,偏移量的操作。
     NSFileHandle基本步骤:
     
     1. 打开文件,获取一个NSFileHandle对象。
     2. 对打开NSFileHandle的文件对象行I/O操作
     3. 关闭文件对象

 - 简单对象的读写(I/O)操作

     iOS中提供四种类型(包括其子类型)可以直接进行文件存取:
     
     1. NSString
     2. NSDictionary
     3. NSArray
     4. NSData

     其基本操作如下:
     
     ``` 
     // 在Documents下面创建一个文本路径,假设文本名称为objc.txt
     NSString *txtPath = [docPath stringByAppendingPathComponent:@"objc.txt"]; // 此时仅存在路径,文件并没有真实存在
     NSString *string = @"Objective-C";
 // 字符串写入时执行的方法
 [string writeToFile:txtPath atomically:YES encoding:NSUTF8StringEncoding error:nil];
 NSLog(@"txtPath is %@", txtPath);
 // 字符串读取的方法
 NSString *resultStr = [NSString stringWithContentsOfFile:txtPath encoding:NSUTF8StringEncoding error:nil];
 NSLog(@"resultStr is %@", resultStr);

     ```

 - 文件管理器

     使用文件管理器(NSFileManager)可以实现对文件进行操作(创建、删除、改名等)以及文件信息的获取

 - 流

     使用Cocoa框架中的输入输出流,可以从文件或应用中内存读取数据,也可以向文件/应用中内存写入数据。还可以用于socket的数据交互处理。
     
     其主要类与方法如下:
     
     ![NSStream的主要类与方法](http://upload-images.jianshu.io/upload_images/6836572-9f75de0b6c31847d.jpg?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)
     
## 序列化和反序列化


前面提到过,NSArray,NSDictionary,NSString,NSNumber,NSDate,NSData以及它们的可变版本(指NSMutableArray,NSMutableDictionary...这一类) ,都可以方便的将自身的数据以某种格式(比如xml格式)序列化后保存成本地文件。

但是如果用于存放数据的类是自己定义的,并不是上面这些预置的对象,如自定以的Person类,像这种自定义的类是无法在程序内部通过writeToFile这个方法写入到文件内

既然复杂对象无法使用内部方法进行数据持久化,那么只能通过将复杂对象转换成NSData,然后在通过上面的方法写入文件,而这种转换的步骤就被称为归档,从文件中读取NSData数据,将NSData转换为复杂对象,这个步骤就是反归档。

- 要点

 - 复杂对象写入文件的过程(复杂对象->归档->NSData->writeToFile)
 - 从文件中读取出复杂对象过程(读取文件->NSData->反归档->复杂对象

- 实现步骤

 1. 首先,复杂对象所属的类要遵守<NSCoding>
 2. 其次,实现协议中的两个方法:
     - -(void)encodeWithCoder:(NSCoder *)aCoder; 序列化
     - -(id)initWithCoder:(NSCoder *)aDecoder; 反序列化

- 例子

 1. 首先,遵守NSCoding协议

     ```
     @interface Person:NSObject<NSCoding> 
         @property(nonatomic,copy) NSString *name 
         @property(nonatomic,assign) integer age; 
 @end
 
     ```
 2. 其次,实现协议中的两个方法:

     ```
     // 对person对象进行归档时,此方法执行。
     // 对person中想要进行归档的所有属性,进行序列化操作。
     -(void)encodeWithCoder:(NSCoder *)aCoder
 {
  [aCoder encodeObject:self.name forKey:@"name"];
 [aCoder encodeInteger:self.age forKey:@"age"];
 }
 // 对person对象进行反归档时,该方法执行。
 // 创建一个新的person对象,所有属性都是通过反序列化得到的。
 -(id)initWithCoder:(NSCoder *)aDecoder 
 {
   self = [super init];
   if (self) {
     self.name = [aDecoder decodeObjectForKey:@"name"];
    self.age = [aDecoder decodeIntegerForKey:@"age"];
  }
  return self;
 }
 // 准备一个NSMutableData, 用于保存归档后的对象
 NSMutableData *data = [NSMutableData data];
 // 创建归档工具
 NSKeyedArchiver *archiver = [[NSKeyedArchiver alloc] initForWritingMutableData:data];
 // 归档
 [archiver encodeObject:p] forKey:@"p1"];
 // 结束
 [archiver finishEncoding];
 // 拼音写入沙盒路径
 NSString *caches = NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES)[0];
NSString *filePath = [caches stringByAppendingPathComPonent:@"person"];
// 写入沙盒
[data writeToFile:filePath atomically:YES];
// 反归档
// 从filePath文件路径读取
NSData *data = [NSData dataWithContentsOfFile:filePath];
// 反归档工具
NSKeyedUnarchiver *unArchiver = [[NSKeyedUnarchiver alloc] initForReadingWithData:data];
// 反归档成对象
Person *p2 = [unArchiver decodeObjectForKey:@"p1"];
// 反归档结束
[unArchiver finshDeoding];  
 
     ```
     
## 面向对象特性

- 继承&多态

objc的这两个特性除了使用语法上,与其他oo语言没有什么什么不同。基本语法之前也有提过,@interface用于声明一个类,@implementation用于类的定义。

不过需要值得注意的一点是,由于objc的动态消息传递机制,objc中不存在真正意义上的私有方法。但是如果方法不再.h文件中声明,而只在.m文件中实现,基本上也和私有方法差不多。

所以在使用objc时,一般会将公有的放到.h文件中,将私有的放到.m文件中。实现私有一般用类扩展实现。

- 属性

在属性方面,objc相比于其他oo语言,有自己的语法糖,@Property和@synthesize。简单来说,.h文件中使用@Property, .m文件中使用@synthesize,编译器会自动创建成员属性以及对应的get和set方法。

比较值得关注的,是@Property的几个特性。

 - 原子性

     1. atomic(默认使用)。使用该选项,保证调用者对变量的访问是线程安全的,在多线程环境下会返回一个多多个值(其他线程前后),而不是一个垃圾值。

     2. noaomic。如其名,使用该关键字后,不能保证线程安全,但是性能及访问效率优于前值。所以在单线程环境下一般指定该关键字。

 - 寄存器控制

   1. readwrite(默认):readwrite是默认值,表示该属性同时拥有setter和getter。

  2. readonly: readonly表示只有getter没有setter。

 - 内存管理 

   1. assign(默认):assign用于值类型,如int、float、double和NSInteger,CGFloat等表示单纯的复制。

      其在set的实现,是采用直接赋值来实现设值操作的

     ```
       -(void)setVar:(int)newVar{  
         var= newVar;  
     } 
     ```

   2. retain:在set方法中,需要对传入的对象进行引用计数加1的操作

     简单来说,就是对传入的对象拥有所有权,只要对该对象拥有所有权,该对象就不会被释放。如下代码所示:

    ```
   -(void)setName:(NSString*)_name{  
  if ( name != _name){  
       [name release];  
       name = [_name retain];  
  }  
}
   ```

     首先判断是否与旧对象一致,如果不一致进行赋值。之所以要增加if判断,是因为如果是同一个对象的话,进行if内的代码会造成一个极端的情况:当此name的retain为1时,使此次的set操作让实例name提前释放,而达不到赋值目的

3. strong:表示实例变量对传入的对象要有所有权关系,即强引用。strong跟retain的意思相同并产生相同的代码,但是语意上更好更能体现对象的关系。

4. weak:在set方法中,需要对传入的对象不进行引用计数加1的操作。
简单来说,就是对传入的对象没有所有权,当该对象引用计数为0时,即该对象被释放后,用weak声明的实例变量指向nil,即实例变量的值为0。

5. copy:与strong类似,但区别在于实例变量是对传入对象的副本拥有所有权,而非对象本身

- Category

这算是oc的一个比较有意思特性。如果我们想给一个已存在的、很复杂的类添加一个新的方法(包括系统类)。一般来说,对于自定义类,我们会找源码,然后添加新方法。但是如果我们新增的逻辑也很复杂,这样就会扩大原始设计的规模,有可能会打乱整个设计的结构。

Category就是oc提供的为我们解决这一问题的方法。它可以让我们动态的在已经存在的类中添加新的方法。对类进行扩展时不需要访问其源码,也不需要创建子类。

Category的实现很简单,举个例子。

 ```
 // Deck.h

 #import <Foundation/Foundation.h>
 #import "Card.h"

 @interface Deck : NSObject

 - (Card *)randomDrawCard;

 @end
 ```
 
 这是类Deck的声明文件,其中包含一个实例方法randomDrawCard,如果我们想在不修改原始类、不增加子类的情况下,为该类增加一个drawCardFromTop方法,只需要定义两个文件Deck+DrawCardFromTop.h和Deck+DrawCardFromTop.m,在声明文件和实现文件中用()把Category的名称括起来即可,声明文件如下:

 ```
 // Deck+DrawCardFromTop.h

 #import "Deck.h"
 #import "Card.h"

 @interface Deck(DrawCardFromTop)

 - (Card *)drawCardFromTop;

 @end
 ```

 实现文件如下:

 ```
 // Deck+DrawCardFromTop.m

 #import "Deck+DrawCardFromTop.h"
 #import "Card.h"

 @implementation Deck(DrawCardFromTop)

 - (Card *)drawCardFromTop
 {
     //TODO.....
 }

 @end
 ```

 DrawCardFromTop是Category的名称。这里一般使用约定俗成的习惯,将声明文件和实现文件统一采用”原类名+Category名”的方式命名。
使用也非常简单,引入Category的声明文件,然后正常调用即可:

 ```
 // main.m

 #import "Deck+DrawCardFromTop.h"
 #import "Card.h"

 int main(int argc, char * argv[])
 {

     Deck *deck = [[Deck alloc] init];
     Card *card = [deck drawCardFromTop];

    return 0;

 }
 ```

 使用类别(Category),不仅在团队协作开发时带来方便(至少不会因为同时更改一个文件,svn更新后需要自己解决冲突)。当一些基础类库满足不了我们的需求时我们还可以拓展基础类库。

 举个例子,如果我们要分割一个字符串,然后用一个NSArray记录每个分割子串的长度。也就是说,NSArray每个元素保存子NSstring的length。但是,如之前提到,NSArray只能保存NS对象,基本值类型(在这里是int)无法保存,扎心了。于是我们每次都要先获取子Nstring的length,然后转换为NSnumber,再保存于数组中。此时,我们完全可以扩展NSstring,使其在获取长度时返回NSnumber对象。
 
 使用Category,还可以实现类扩展。前面说过,objc中定义私有的属性和方法,一般用class extension实现。其特点如下:
 
 - 不需要名字
 - 可以在自己的类中使用
 - 可以添加实例变量
 - 可以将只读权限修改为可读写权限
 - 创建数量不限

 举个例子:
     ```
   //Things.h
 @interface Things : NSObject
 @proterty (assign) NSInteger thing1;
 @ptoterty (readonly, assign) NSInteger thing2;
 -(void)resetAllVal;
 @end
     ```

    ```
   //Things.m
 
 @interface Things(){
     NSInteger thing4;
 }
 
 @proterty (readwrite, assign) NSInteger thing2;
 @proterty (assign) NSInteger thing3;
 
 @end
 
 @implementation
 
 ...
 
 @end
 ```
 
 我们使用了类扩展,添加了私有实例变量和私有属性,还修改了thing2的读写权限,其对外只提供读,对内可读写。

 但是Category不是万能的,Category可以访问原始类的实例变量,但不能添加变量,如果想添加变量,可以考虑通过继承创建子类。
 
 - 类扩展与类别的区别:
     
     1. 类别中只能增加方法
     
     2. 类扩展不仅可以增加方法,还可以增加实例变量(或者合成属性),只是该实例变量默认是@private类型的(作用范围只能在自身类,而不是子类或其他地方);
     3. 类扩展中声明的方法没被实现,编译器会报警,但是类别中的方法没被实现编译器是不会有任何警告的。这是因为类扩展是在编译阶段被添加到类中,而类别是在运行时添加到类中。
     4. 类扩展不能像类别那样拥有独立的实现部分(@implementation部分),也就是说,类扩展所声明的方法必须依托对应类的实现部分来实现。

- 匿名函数(block)

 objc中的block相当于c中的函数指针。二者仍有一定区别,如下
 
 - block的代码是内联的,效率高于函数调用
 - block对于外部变量默认是只读属性
 - block被Objective-C看成是对象处理

 block声明和定义语法如下图所示。

 ![objc声明与语法定义](http://upload-images.jianshu.io/upload_images/6836572-f34f5ff05a3b806b.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)
 
 block特性如下
 
 - 捕获外界便变量

     ```
     CGPoint center = cell.center;
     CGPoint startCenter = center;
     startCenter.y += LXD_SCREEN_HEIGHT;
     cell.center = startCenter;

     [UIView animateWithDuration: 0.5 delay: 0.35 *      indexPath.item usingSpringWithDamping: 0.6      initialSpringVelocity: 0 options:       UIViewAnimationOptionCurveLinear animations: ^{
     cell.center = center;
     } completion: ^(BOOL finished) {
     NSLog("animation %@ finished", finished? @"is": @"isn't");
     }];
     ```

     这里面就用到了void(^animations)(void)跟void(^completion)(BOOL       finished)两个block,系统会在动画开始以及动画结束的时候分别调用者两个       block。在实现动画的block内部,代码访问了上文中的center属性——在动画开     始的时候这个动画函数的生命周期早已结束,而block会捕获代码外的局部变量,      当然这只局限于只读操作。如果我们在block中修改外部变量,编译器将会报错。
     
     同时,block在捕获变量的时候只会保存变量被捕获时的状态(对象变量除外),之后即便变量再次改变,block中的值也不会发生改变。见下面代码:
     
     ```
     CGPoint center = CGPointZero;
     CGPoint (^pointAddHandler)(CGPoint addPoint) = ^(CGPoint addPoint) {
     return CGPointMake(center.x + addPoint.x, center.y + addPoint.y);
}
     center = CGPointMake(100, 100);
     NSLog(@"%@", pointAddHandler(CGPointMake(10, 10)));    //输出{10,10}
     
     ```
     
     要想在block内部修改外部变量,可以给变量增加 _block关键字。     
     
 - 循环引用

     前面说过,block在iOS开发中被视作是对象,因此其生命周期会一直等到持有者     的生命周期结束了才会结束。另一方面,由于block捕获变量的机制,使得持有       block的对象也可能被block持有,从而形成循环引用,导致两者都不能被释放:
 
     ```
     @implementation LXDObject
     {
        void (^_cycleReferenceBlock)(void);
     }

     - (void)viewDidLoad
     {
       [super viewDidLoad];
       _cycleReferenceBlock = ^{ 
      NSLog(@"%@", self);   //引发循环引用
      };
     }

     @end

     ```
 
     这种情况最后会导致内存泄露,两者都无法释放。跟普通变量存在__block关键字     一样的,系统提供给我们__weak的关键字用来修饰对象变量,声明这是一个弱引用     的对象,从而解决了循环引用的问题。
 
     ```
     __weak typeof(*&self) weakSelf = self;
     _cycleReferenceBlock = ^{ 
     NSLog(@"%@", weakSelf);   //弱指针引用,不会造成循环引用
     };
     
     ```
     
## 异常、错误处理、断言、日志和调试支持,对单元测试的支持


- 异常

 老生常谈的try catch,与其他oo语言一样,不再多述。
 
 ```
  @try {
     // do something that might throw an exception
 }
 @catch (NSException *exception) {
     // deal with the exception
 }
 @finally {
     // optional block of clean-up code
     // executed whether or not an exception occurred
 }
 
 ```
 
- 错误处理

 NSError是objc的系统错误信息类。其有三个较重要的私有变量:
 
 - code
 
     是一个整数,最好是一个枚举,和特定的错误域是对应的。

 - domain

     一个字符串,标记错误域。
     
 - userInfo

     一个字典,包括任意的键值对。其中有:
     
     1. NSLocalizedDescriptionKey:本地化的错误描述
     2. NSLocalizedRecoverySuggestionErrorKey:本地化的恢复建议
     3. NSLocalizedFailureReasonErrorKey:本地化的失败原因

 NSError主要有两个用法:
 
 - 获取错误信息

     ```
     //获取错误  
     NSError *error = nil;  
     BOOL success = [[NSFileManager defaultManager]      contentsOfDirectoryAtPath:@"path" error:&error];  
     if (!success) {  
      NSLog(@"%@", [error localizedDescription]);  
     } 
     ```

 - 编辑错误信息
     ```
     //预定义信息  
     #define JohnnyErrorDomain       @"com.JohnnyError.Domain"  

     typedef NS_ENUM(NSInteger, ErrorFail){  
      ErrorOne = 1,  
      ErrorTwo,  
      ErrorThree  
     };  
     ```
     
     ``` 
     //产生错误信息  
     NSDictionary *userInfo = @{  
                        NSLocalizedDescriptionKey: NSLocalizedString(@"Operation fail", nil),  
                        NSLocalizedFailureReasonErrorKey: NSLocalizedString(@"The operation timed out.", nil),  
                        NSLocalizedRecoverySuggestionErrorKey: NSLocalizedString(@"Have you tried turning it off and on again?", nil)  
                        };  
     NSError *error = [NSError errorWithDomain:JohnnyErrorDomain  
                                  code:2  
                              userInfo:userInfo];  
     //提示  
     [[[UIAlertView alloc]   initWithTitle:error.localizedDescription  
                             message:error.localizedRecoverySuggestion  
                             delegate:nil  
                             cancelButtonTitle:NSLocalizedString(@"OK", nil)  
                             otherButtonTitles:nil, nil nil] show];  
     
     ```

- 断言

 - NsAsest 

     NSAssert()是一个宏,用于开发阶段调试程序中的Bug,通过为NSAssert()传       递条件表达式来断定是否属于Bug,满足条件返回真值,程序继续运行,如果返回       假值,则抛出异常,并且可以自定义异常描述。
     NSAssert()是这样定义的
     
     ```
     #define NSAssert(condition, desc)
     
     ```

     NSAssert用法
     
     ```
     int a = 1;
     NSCAssert(a == 2, @"a must equal to 2"); //第一个参数是条件,如果第一个参数不满足条件,就会记录并打印后面的字符串
     
     ```

 - NSParameterAssert

     NSAssert和 NSParameterAssert的区别是前者是针对条件断言, 后者只是针对参数是否存在的断言, 调试时候可以结合使用,先判断参数,再进一步断言,确认原因.
     
     NSParameterAssert用法
     
     ```
         - (void)assertWithPara:(NSString *)str
 {
   NSParameterAssert(str); //只需要一个参数,如果参数存在程序继续运行,如果参数为空,则程序停止打印日志
 //further code ...
 }
     
     ```
     
 - 自定义NSAssertionHandler

     Objc中的断言处理使用的是 NSAssertionHandler。
每个线程拥有它自己的断言处理器,它是 NSAssertionHandler 类的实例对象。NSAssertionHandler实例是自动创建的,用于处理错误断言。如果 NSAssert和NSCAssert条件评估为错误,会向 NSAssertionHandler实例发送一个表示错误的字符串。每个线程都有它自己的NSAssertionHandler实例。
我们可以自定义处理方法,从而使用断言的时候,控制台输出错误,但是程序不会直接崩溃。

 ```
     #import "MyAssertHandler.h"

     @implementation MyAssertHandler

     //处理Objective-C的断言
     - (void)handleFailureInMethod:(SEL)selector object:     (id)object file:(NSString *)fileName lineNumber:        (NSInteger)line description:(NSString *)format,...
         {
       NSLog(@"NSAssert Failure: Method %@ for object %@ in      %@#%li", NSStringFromSelector(selector), object, fileName,      (long)line);
     }
     //处理C的断言
     - (void)handleFailureInFunction:(NSString *)functionName        file:(NSString *)fileName lineNumber:(NSInteger)line        description:(NSString *)format,...
     {
       NSLog(@"NSCAssert Failure: Function (%@) in %@#%li",      functionName, fileName, (long)line);
     }

     @end
 
 ```
 
 给线程添加处理类
 
 ```
 NSAssertionHandler *myHandler = [[MyAssertHandler alloc] init];
//给当前的线程
[[[NSThread currentThread] threadDictionary] setValue:myHandler
                                            forKey:NSAssertionHandlerKey];

 ```

 自定义NSAssertionHandler后,程序能够获得断言失败后的信息,但是程序可以继续运行,不会强制退出程序.

- 日志
 
 objc中日志输出处理主要使用的是NSLog,要在日志输出信息中添加上下文信息,编译器提供了常用的表达式。

| Expression  | Format Specifier | Description |
 |-----------------|------------------------|--- -------------|
 |NSStringFromSelector(_cmd)      | %@ |         当前选择器的名字 |
 | NSStringFromClass([self class])     | %@        |           当前对象类的名字 |
 | [[NSString stringWithUTF8String:\__FILE__] lastPathComponent] | %@        |            源码文件的名称|
 | [NSThread callStackSymbols] | %@ |    当前栈信息的刻度字符串数组。仅用于调试,不用向终端用户展示或者在代码中用作任何逻辑。|


- 单元测试
 
 objc中可以使用OCUnit(即用XCTest进行测试)其实就是苹果自带的测试框架。
 一般测试用例分为三个阶段:排列资源、执行行为、断言结果。
 
 - 排列资源

     排列资源,便是提供一切测试方法所需要的东西,而这些东西便称之为资源。这些资源包括:

     1. 方法的输入参数
     2. 方法所执行的特定上下文
 
     这个阶段相当于准备阶段,一切都是为了这个用例中执行行为而作准备,如果没有任何需要准备的数据,这个阶段是可以被忽略的。

 ```
     - (void)test_setObject$forKey {
 // arrange
 NSString *key = @"test_key";
 NSString *value = @"test_value";
 NSMutableDictionary *dic = [NSMutableDictionary new];
}
     
     ```

 - 执行行为

     当准备阶段完毕后,便进入要测试行为的执行阶段,在这个阶段,我们会使用准备好的资源,并记录下行为的输出以供下个阶段使用。这里的行为输出不一定就是方法执行的返回值,很多时候我们要测试的方法并没有任何返回值,但一个方法执行后,总归会有一个预期的行为会发生,即便是空方法也是(什么都不会被改变),而这个预期行为便是测试行为的输出。

     加入执行行为的代码:  
  ```
  - (void)test_setObject$forKey {
 // arrange
 NSString *key = @"test_key";
 NSString *value = @"test_value";
 NSMutableDictionary *dic = [NSMutableDictionary new];

  // act
 [dic setObject:value forKey:key];
 }
     
 ```

 - 断言结果

     最后一步,也是最核心的一步,它决定着一个测试用例的成功与否,我们需要在这一步断言执行行为的输出是否达到预期。确定一个行为的输出,我们可能需要有多次断言,这里需要遵循一个原则:**先执行的断言,不应该以后执行的断言成功为前提**。
     
     ```
         - (void)test_setObject$forKey {
 // arrange
 NSString *key = @"test_key";
 NSString *value = @"test_value";
 NSMutableDictionary *dic = [NSMutableDictionary new];

     // act
   [dic setObject:value forKey:key];

    // assert
   XCTAssertNotNil([dic objectForKey:key]);
   XCTAssertEqual([dic objectForKey:key], value);
     }
     
     ```
     
     可以看到,最后我们是先断言是否为空,再断言是否相等,后者是在前者成功的前提下才可能不失败。如果颠倒顺序,就很难尽早的发现错误原因。
     
## RunTime


Objc是一门动态语言,所以它总是想办法把一些决定工作从编译连接推迟到运行时。也就是说只有编译器是不够的,还需要一个运行时系统 (runtime system) 来执行编译后的代码。

- 使用RunTime的场景

 OC程序使用Runtime 系统有三种情景:Objective-C Source Code、NSObject Methods、Runtime Functions;

 - Objective-C Source Code
 
 - NSObject Methods

 - Runtime Functions

- 消息机制

 上面提到过,RunTime的实质就是消息的发送。在objc中,调用方法:
 
 ```
 [receiver messge]
 ```
 在编译器中会转换成消息机制里的消息发送形式
 
 ```
 objc_msgSend(receiver, selector)
 
 //带参数
 objc_msgSend(receiver, selector, arg1, arg2...)
 ```
 
 消息功能为动态绑定做了很多必要的工作:
 
 1. 通过selector在消息接收者class里选择方法实现(method implement)
 2. 调用方法实现,传递到接收对象with参数
 3. 传递方法实现返回值
 
 为了让编译器编译时,消息机制与类的结构关联上,每个类的结构里添加了两个基本的元素:
 
 1. 指向父类的指针(isa指针)
 2. 类调度表(A class dispatch table),通过Selector方法名在dispatch table里面匹配对应的方法地址(class-specific address)

 当一个对象被创建并分配内存时,它的实例里的变量会初始化,里面有一个指向它的类的结构体,isa指针。
 
 消息发送到一个对象时,通过class结构体里的isa指针在dispatch table里寻找相应的selector,如果找不到便进入父类里找,一直找到NSObject,一旦定位到selector,便调用该方法,传递相关数据。为了提高效率,RunTime会缓存调用过的selector和方法地址,在到dispatch table查找之前,先到cache里查找。


## 如何进行callback方法调用,如何支持事件驱动编程模型


- 非正式协议

 引用《Cocoa设计模式》
 
 > 非正式协议通常定义为NSObject的类别。类别接口中指定的方法可能会或者可能不会被框架类实际地实现。非正式协议位于一种设计灰区中。正式协议由编译器检查并且代表一种关于对象能力的保证,但是非正式协议不会做出保证----而只会给出提示。
 
 引用官方文档
 
 > An informal protocol is a category on NSObject, which implicitly makes almost all objects adopters of the protocol. (A category is a language feature that enables you to add methods to a class without subclassing it.) Implementation of the methods in an informal protocol is optional. Before invoking a method, the calling object checks to see whether the target object implements it. Until optional protocol methods were introduced in Objective-C 2.0, informal protocols were essential to the way Foundation and AppKit classes implemented delegation.
 
 可以看出,非正式协议就是类别,凡是NSObject或其子类的类别,都是非正式协议。
 
 
- 正式协议

 正式协议从概念上理解起来就简单的多了,它指的是一个以@protocol方式命名的方法列表,与非正式协议相比不同的是,它要求显示的采用协议。
 
 - 正式协议的声明

     1. @required 该类的方式在遵守相应协议的类中是必须被实现的,不然编译器会警告(显然这是在编译时做的检查,而不是在运行时)
     2. @optional 该类的方法在遵守相应协议的类中是否实现是可选的,@optional已取代非正式协议

 - 正式协议的继承性

     正式协议和类一样,是可以继承的,书写格式同类继承相似:
     
     ```
     @protocol NewProtocal  <Protocal>  
     @end  
     
     ```
 

- 委托方法
 
 委托概念什么的这里略过。下面主要叙述如何在objc中定义和使用委托。
 
 - 定义委托

     ```
     #import <BlaClass/BlaClass.h>

     @class MyClass;             //定义类,这样协议可以看到MyClass
     @protocol MyClassDelegate   //定义委托协议
     - (void) myClassDelegateMethod: (MyClass *) sender;  //定义在另一个类里实现的委托方法
     @end //结束协议

     @interface MyClass : NSObject {
     }
     @property (nonatomic, weak) id <MyClassDelegate> delegate; //定义 MyClassDelegate为委托

     @end
     
     ```
     
 - 定制委托

     ```
     #import "MyClass.h"
@interface MyVC:UIViewController <MyClassDelegate> { //make it a delegate for MyClassDelegate
}
     
     ```
     
     ```
     myClass.delegate = self;          //设置委托至自身的某个地方
     
     ```
     
 - 使用委托, 如下形式

     ```
     if([[self delegate] respondsToSelector:@selector(windowDidMove:)]) {
   [[self delegate] windowDidMove:notification];
}
     
     ```

- 响应选择器selector

 selector,实际上是函数指针的一种实现形式,我们用一个 C string 来表示对象中的某个函数,所以就可以把这个函数作为参数,传到其他的方法中去进行调用。
 
 Objective-C 的 Class 在编译时会变成 C struct,Class 中包含的方法也会转换成 C function。之后在运行的时候,runtime 会建立起从 Objective-C Method 到 C function 的映射(可以认为是一个 virtual table)。
 
 Runtime 会为每个类准备一个 virtual table,里面是一个个键值对,key 称为 selector,类型是 SEL,value 实际上是 C function 的函数指针,类型是 IMP。而这里的 SEL 类型实际上就是 C string。
 
 因此 selector 可以看做是函数的另一个名字,所以很多需要调用函数或者建立连接的地方,都可以用到。

- objc的回调实现

 - Run loop

     objc提供的NSRunLoop实例会持续等待着,当特定事件发生时,触发回调(callback)。

     调用以下方法,即可得到一个run loop。
     
     ```
     [[NSRunLoop currentRunLoop] run];
     ```

 - target-action/目标-动作对

     实例:
     
     ```
     // 为按钮添加回调——Target-action/目标-动作对
// 第一个参数:发送消息给谁
// 第二个参数:事件发生后,执行什么代码(回调)
// 第三个参数:发生哪类型的点击事件会触发回调
[button addTarget:self
        action:@selector(click:)
forControlEvents:UIControlEventTouchUpInside];

     ```
     
     目标-动作对,就是当事件发生时,像指定的对象发送指定的消息。对target,action的对应理解,可以这样认为,执行某个类(target)的某个方法。
     
 - Helper object/委托

     委托使用如上文所示。
 
 - Notification/通告
     
     objc提供了一个叫做「通告中心」的对象,可以通过[NSNotificationCenter defaultCenter]获得,利用这个通告中心,我们可以「发通告」、「监测(接收)通告」,利用这个机制,实现回调。
 
 - Block

     在objc中使用block实现回调,除了基本声明语法,其他与大多oo语言相同,不再叙述。
     
     ```
     #import <Foundation/Foundation.h>
     @import CoreBluetooth;

     // 步骤1:
     // 将Block重新定义为一种新的数据类型
     // 这个Block无返回值;有一个参数(类型为NSUInteger)
     typedef void(^AllDevicesDidConnectedBlock)(NSUInteger divicesCount);

     @interface MyCnetralManager : NSObject

     // 步骤2:
     // 声明一个(Block)变量
     @property (nonatomic, strong) AllDevicesDidConnectedBlock callbackForAllDevicesDidConnected;

     @end
     ```
     
     然后,就可以使用了。
     
 - 总结

     1. 当只发生单个事件(event),只需要完成一件事情进行响应,建议用「Target-action/目标-动作对」。比如NSTimer、UIButton等。
     2. 当会发生若干事件(event),要完成多件事情进行响应,建议使用「Helper objects/辅助对象」,当然了,最常见的是「delegate/委托」(另外还有「data sources/数据源」)。
     3. 当发生单个事件(event),多个对象要进行响应,建议使用「Notifications/通告」
     4. 使用Block,可以写出更简洁的代码、更好的代码结构。
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,088评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,715评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,361评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,099评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 60,987评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,063评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,486评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,175评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,440评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,518评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,305评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,190评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,550评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,880评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,152评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,451评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,637评论 2 335

推荐阅读更多精彩内容

  • 转至元数据结尾创建: 董潇伟,最新修改于: 十二月 23, 2016 转至元数据起始第一章:isa和Class一....
    40c0490e5268阅读 1,670评论 0 9
  • *面试心声:其实这些题本人都没怎么背,但是在上海 两周半 面了大约10家 收到差不多3个offer,总结起来就是把...
    Dove_iOS阅读 27,107评论 29 470
  • 她说要给我一个东西,还说这东西代表我们的关系。然后在餐厅考试可我和李豪挨着,做完题后,我就让他帮忙猜一下,然后我俩...
    小狐的冰山阅读 149评论 0 0
  • 再一次听到老牛说话实在yy里,
    飞云阅读 281评论 0 15
  • 说起林心如,一直以来给人的印象就是“古装女神”、“清纯玉女”。她在古装剧《还珠格格》中塑造的紫薇,由于太过经典,至...
    进击的小狮妹阅读 834评论 0 0