第5部分 Objective-C 高级主题

作者: Sober_DeTong | 来源:发表于2017-03-01 20:34 被阅读28次
    第33章 init

    向新创建的对象发送init消息,它就会初始化其下的实例变量。也就是说,alloc负责分配对象空间,init负责初始化对象。请注意init是实例方法,返回的是初始化后的对象地址。

    - (instancetype)init {
      //调用NSObject的init方法
      self = [super init];
      //是否返回非nil的值?
      if (self) {
        //  为实例变量赋值
        _voltage = 120;
      }
    return self;
    }```
    这个init方法会返回一个instancetype类型的值。instancetype这个关键字会告诉编译器方法返回什么类型的对象。你编写的或是覆盖的任何初始化方法都应该返回instancetype类型的值。
    
    在还没有引入instancetype之前,初始化方法返回的都是id类型。然而,使用instancetype是更好的选择,它除了灵活解决子类的问题,还可以让编译器检查返回值的类型。
    
    在上述的init方法中,头两行代码是做检查:
    * 这个init方法的第一行,将父类的init方法所返回的对象赋给self。
    * 检查父类的初始化方法的返回值,确定不是nil并且有效。
    
    这些检查有什么用处呢?少数类的初始化方法需要做一些特殊处理。下面举两个例子。
    * 出于优化考虑,init方法会释放已经分配了内存的对象,然后创建另一个新对象并返回之。
    对于这种情况,苹果公司要求的做法是:将父类的init方法所返回的对象赋给self。
    * init方法在执行过程中发生了错误,所以会释放对象并返回nil。
    而对于这种情况,苹果公司建议的做法是:检查父类的初始化方法的返回值,确定不是nil并且有效。如果对象不存在,就没有必要执行自定义的初始化方法。
    ***
    创建子类时,通常只需要初始化新的实例变量。此外,也需要调用父类的初始化方法,初始化父类的实例变量。
    ***
    每个类都有一个指定初始化方法(designated initializer)。init 是 NSObject 的指定初始化方法。
    
    指定初始化方法扮演的是单一入口的角色。任何类都有且只有一个指令初始化方法。如果某个类还有其他初始化方法,那么这些方法应该(直接地或间接地)调用指定初始化方法。
    创建新类时,如果指定初始化方法的方法名和父类的不同,就需要在类的头文件中做出说明。加入适当的注释。
    
    编写初始化方法时,应该遵循以下规则:
    * 其他的初始化方法都应该(间接地或直接地)调用指定初始化方法。
    * 指定初始化方法应该先调用父类的指定初始化方法,然后再对实例变量进行初始化。
    * 如果某个类的指定初始化方法和父类的不同(这里指的是方法名不同),就必须覆盖父类的指定初始化方法,并调用新的指定初始化方法。
    * 如果某个类有多个初始化方法,就应该在相应的头文件中明确地注明哪个方法是指定初始化方法。
    ***
    假设要创建一个NSObject的子类。出于安全考虑,必须为其中的一个实例变量赋值,不能使用默认值。
    最佳的解决方案是覆盖父类的指定初始化方法,然后通过某种途径告知程序员不能调用这个方法,并提供修改建议:
    
    • (instancetype)init
      {
      [NSException raise:@"BNRWallSafeInitialization"
      format:@"Use initWithSecretCode:, not init"];
      }```
      这类问题会造成程序的崩溃。如果不按上述方法修改,控制台就会输出崩溃的错误信息。

    第34章 再谈属性
    属性的存取类型:

    任何一个属性可以声明为readwrite或 readonly,默认为readwrite。readwrite代表:程序应该自动创建存方法和取方法。如果无需创建存方法,则可以将属性声明为readonly:
    @property (readonly) int voltage;

    属性的生命周期类型:

    首先我们先区分一下在MRC和ARC下的属性都有使用哪些关于生命周期管理的修饰符。

    MRC: assign, copy, retain
    ARC: strong, weak, unsafe_unretained, copy

    assign: 简单赋值,不更改引用计数。一般用于基础类型的数据(NSInteger)和C语言类型数据(int , float , double , char , bool)。其在MRC下是默认值。

    copy: 会拷贝传入的对象(即创建一个引用计数为1的新对象,但是内容与传入对象相同),并把新对象赋值给实例变量。常用与NSString,NSArray,NSDictionary,NSSet等。

    retain: 释放旧对象,并使传入的新对象引用计数+1。此属性只能用于NSObject及其子类,而不能用于Core Foundation(因为其没有使用引用计数,需要另外使用CFRetain和CFRelease爱进行CF的内存管理)。

    ARC加入的属性修饰符如下。

    strong: 强引用,类似于retain。要求保留传入的对象,并放弃原有对象。一个对象只要被至少一个强引用指向,则其不会被释放,而当没有强引用指向时则会被释放。其在ARC下是对象类型的默认值。

    weak: 弱引用,要求不保留传入的属性(既不会使传入的对象引用计数+1)。类似于assign,但与assign不同的是,当它们指向的对象被释放后,weak会被自动置为nil,而assign则不会,所以assign会导致“野指针”的出现,weak可以避免悬空指针。

    unsafe_unretained: 其实质等同于assign。与weak的区别就是指向的对象如果被释放,其不会被置为nil,而导致悬空指针的出现。它是ARC模式下非对象属性的默认值。

    所以综上所述,属性的默认值主要有以下情况。

    • MRC:(atomic, readwrite, assign)
    • ARC下对象类型属性:(atomic, readwrite, strong)
    • ARC下非对象类型:(atomic, readwrite, unsafe_unretained)

    其它对比:
    1. copy/retain
      答:copy会拷贝创建一个新的对象,并使得它的引用计数为1。retain则是Release旧值,retain新值,其本质是指针复制(浅复制),引用计数加1,而不会导致内容被复制。
      如:一个NSString对象,内存地址为:0x1111,内容为@“Hello”。
      (1)copy到另外一个NSString后,地址为0x2222,内容相同(新建一个内容,内容拷贝),新的对象引用计数为1,旧的对象内容没有改变,引用计数-1。
      (2)retain到另外一个NSString后,地址相同(新建一个指针,指针拷贝),内容相同,对象的引用计数+1。

    2)assign/retain(MRC情况下)
    答:assign只是简单的赋值,如果它引用的对象被释放了,则会造成悬空指针的出现,此时再通过该引用访问对象则会导致程序crash。retain则是在引用计数的基础上,对对象引用计数+1,以获取对象的拥有权,这样只有当对象的引用计数为0时才会被释放(既没有别的引用指向它),这样可以避免访问一个被释放的对象。

    3)assign/weak(ARC情况下,因为assign类似于unsafe_unretained,所以也可以说是weak和unsafe_unretained的区别)
    答:assign不同的是,当它们指向的对象被释放后,weak会被自动置为nil,而assign则不会,所以assign会导致“野指针”的出现。


    扩展:

    上面那些属性描述符都是针对类中定义的属性而言的,实际上对于局部变量也有类似的关键字来修饰变量,常用主要有
    __strong,__weak, __unsafe_unretained, __autoreleasing

    __strong: 是默认引用类型的关键字。
    __weak: 声明一个可以自动置nil的弱引用。
    __unsafe_unretained: 弱引用,但是当指向对象被释放时,不会被置nil。所以会导致野指针的出现。
    __autoreleasing:用来修饰一个函数的参数,这个参数会在函数返回的时候被自动释放。


    对象的拷贝:

    有些类有两个版本:一个是可修改的,另一个是不可修改的。无论是哪个版本,copy方法都会返回不可修改的版本。例如,NSMutableString的copy方法会返回NSString实例。如果要拷贝可修改的对象,就要使用mutableCopy。

    本章以上这部分是摘抄自http://blog.csdn.net/linyousong/article/details/50762199 之内容。由于原书中多以MRC为例,所以找了这个有关ARC和MRC对比版本的帖子。


    copy

    copy特性要求拷贝传入的对象,并将新对象赋给实例变量。
    对不可修改的对象进行复制,好像是在做无用功。
    NSObject的copy方法其实仅仅是调用copyWithZone:,并将nil作为实参传入。不可修改的类通常会覆盖copyWithZone:方法,以优化拷贝过程。以NSString为例,它的copyWithZone:方法的示例代码如下:

    - (id)copyWithZone:(NSZone *)z {
        return self;
    }```
    也就是说,NSString对象不会真的拷贝出一个新对象。
    如果要拷贝出可修改对象,就要使用mutableCopy方法。
    
    Objective-C没有为属性提供mutableCopy这样的特性。如果某个存方法需要复制传入的对象,并且要求新对象是可修改的,就必须自己编写代码实现(向传入的对象发送mutableCopy消息),而不能依赖属性机制。
    ######再谈对象拷贝
    大多数不是来自苹果公司的Objective-C类并没有实现copyWithZone:方法。
    NSObject类的copy方法和mutableCopy方法的实现代码大致如下:
    
    • (id)copy {
      return [self copyWithZone:NULL];
      }
    • (id)mutableCopy {
      return [self mutableWithZone:NULL];
      }```
      copyWithZone:方法以及mutableWithZone:方法分别在NSCopying与NSMutableCopy协议中进行了声明。
      如果希望你的类可以使用copy特性的属性,就需要确保它们符合NSCopying协议。
    实现存取方法

    如果你声明一个属性,手动实现存取方法,编译器就不会合成实例变量。
    但如果你需要实例变量,就必须自己创建。创建的方法是在类的实现文件中添加@synthesize。代码如下:

    @property (nonatomic , copy) NSString *listName;
    ...
    @implementation ClassName
    @synthesize listName = _listName;
    

    @synthesize指令会告诉编译器有一个叫做_listName的实例变量,它是listName以及setListName的实例变量,如果它不存在,就要将它创建出来。
    但如果只写一个@synthesize指令,编译器就会警告说_listName是未经定义的。
    声明一个只读属性时,编译器会自动合成一个取方法和一个实例变量。因此,如果手动给只读属性实现取方法,效果和读/写属性实现存取方法是一样的。编译器不会合成实例变量,需要手动合成它。
    声明属性仍然是声明存取方法的快捷方法,而且它会给代码带来视觉上的连贯性。

    第35章 KVC

    KVC(key-value coding)能够让程序通过名称直接存取属性。因为与KVC有关的方法都是在NSObject中定义的,所以凡是继承自NSObject的类都具备KVC功能。

    [a setValue:@"Washing Machine" forKey:@"productName"];
    

    在这行代码中,setValue: forKey:方法会查找名为setProductName:的存方法。如果对象a没有setProductName:方法,就会直接为实例变量赋值。

    [a valueForKey:@"productName"];
    

    在这段代码中,valueForKey:方法会查找名为productName的取方法。如果对象没有productName方法,就会直接返回相应的实例变量。
    如果输错了属性的名称,编译器并不会发出警告,但是在运行时会发生错误。
    为什么需要KVC,有什么好处?当苹果公司提供的某个框架需要向你编写的对象写入数据时,会使用setValue:forKey:。当苹果公司提供的某个框架需要从你编写的对象读取数据时,会使用valueForKey:。以CoreData框架为例(CoreData框架能够将对象保存在SQLite数据库中,并在需要时将其还原成对象),这套框架会通过KVC来管理自定义的数据对象。
    虽然程序没有实现针对_productName 的存取方法,但是,通过KVC,其他(对象外部的)方法一样可以存取_productName。这明显违背了对象封装(object encapsulation)理念。所谓的对象封装是指对象的方法可以公开,但是实例变量应该保持私有。KVC是一个例外。

    非对象类型

    KVC只对对象有效,但是有些属性的类型并不是对象,例如int或float。如何通过KVC存取这些属性?答案是使用NSNumber对象。

    [a setValue:[NSNumber numberWithInt: 240] forKey:@"voltage"];
    
    Key路径

    大部分应用最后都会有一个相对复杂的对象表。
    使用key路径(key path),可以让系统帮你遍历关系。将你想要的key排成一个长串,以点分隔。注意顺序很重要,第一个想要遍历的对象放在第一个:

    NSString *numberToDial = [a valueForKeyPath:@"manager.emergencyContact.phoneNumber"];
    
    第36章 KVO

    键-值观察(key-value observing)是指当指定的对象的属性被修改时,允许对象接受通知的机制。虽然它不是很常用,但它是Cocoa bindings以及CoreData的关键组成部分。

    [a addObserver:observer
        forKeyPath:@"lastTime"
           options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld
           context:nil];
    //  lastTime属性在类a的里面
    //  在observer类里面实现发生变化时的回调方法
    - (void)observeValueForKeyPath:(NSString *)keyPath 
                          ofObject:(id)object
                            change:(NSDictionary<NSKeyValueChangeKey,id> *)change 
                           context:(void *)context {
        NSLog(@"%@",keyPath);
        NSString *oldValue = [change objectForKey:NSKeyValueChangeOldKey];
        NSString *newValue = [change objectForKey:NSKeyValueChangeNewKey];
    }
    
    在KVO中使用context

    假如父类(设为ClassA)和子类(设为ClassB)都监听了同一个对象肿么办?
    可以创建一个单独的指针,在开始观察的时候将它作为context,每次收到通知的时候将它和context进行对比。静态变量的地址可以很好地工作。因此,如果子类化某个使用了KVO的类时,可以编写如下代码:

    static int contextForKVO;
    ...
        [obj addObserver:self
              forKeyPath:@"fido"
                 options:NSKeyValueObservingOptionNew
                 context:&contextForKVO];
    ...
    - (void)observeValueForKeyPath:(NSString *)keyPath
                          ofObject:(id)object
                            change:(NSDictionary<NSKeyValueChangeKey,id> *)change
                           context:(void *)context
    {
        NSLog(@"%@",keyPath);
        NSString *oldValue = [change objectForKey:NSKeyValueChangeOldKey];
        NSString *newValue = [change objectForKey:NSKeyValueChangeNewKey];
        //  这不是我的?
        if (context != &contextForKVO) {
            //  将它传递给父类
            [super observeValueForKeyPath:keyPath ofObject:object change:change context:context];
        }else {
            //  处理其它变化
        }
    }
    
    显示触发通知

    如果使用存取方法来设置属性,那么系统会自动通知观察者。但如果出于某些原因,你选择不使用存取方法呢?这是可以通过willChangeValueForKey:didChangeValueForKey:方法通知系统某个属性的值即将/已经发生变化。

    - (void)updateLastTime:(NSTimer *)t {
        [self willChangeValueForKey:@"lastTime"];
        _lastTime = @"newValue";
        [self didChangeValueForKey:@"lastTime"];
    }
    
    独立的属性

    如果你不想观察_lastTime而想观察_lastTimeString,该怎么办?

    [a addObserver:observer
        forKeyPath:@"lastTimeString"
           options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld
           context:nil];
    

    系统不知道当_lastTime发生变化的时候,_lastTimeString也会发生变化。
    为了修复这个问题,你可以告诉系统_lastTime会影响_lastTimeString,可以通过实现一个类方法来做这项工作。

    + (NSSet *)keyPathsForValuesAffectingLastTimeString {
        return [NSSet setWithObject:@"lastTime"];
    }
    

    请注意这个方法的名字:它是keyPathsForValuesAffecting加上首字母大写的键的名字。类似属性的存方法是set加上首字母大写的属性名。
    系统会在运行时找到它。

    关于KVO的其它一些博客:
    http://southpeak.github.io/2015/04/23/cocoa-foundation-nskeyvalueobserving/
    这上面记载的也很不错。

    第37章 范畴

    通过使用范畴(Category),程序员可以为任何已有的类添加方法。
    范畴的方法会替换之前存在的方法。所以命名的时候增加前缀是一个很好的习惯。
    应该使用范畴来给已存在的类增加新方法,而不要在范畴中替换已存在的方法;这种情况下应该创建该类的子类。

    相关文章

      网友评论

        本文标题:第5部分 Objective-C 高级主题

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