Effective Object C 2.0 『对象、消息、运行期』

第6条:属性(property)

@interface SomeClass : NSObject

@property NSString *instanceObj;

@end
复制代码

上面代码写出来的类与下面是等价的:

 @interface SomeClass : NSObject 

- (NSString*)instanceObj;
- (void)setInstanceObj:(NSString *)instanceObj;

@end
复制代码

因为声明了instanceObje属性

@property NSString *instanceObj;

相当于在头文件生成getter、setter方法头,如下:

- (NSString*)instanceObj;
- (void)setInstanceObj:(NSString *)instanceObj;
复制代码

以及在.m实现文件添加了实例变量_instanceObj,及getter、setter方法的具体实现,如下:

@synthesize instanceObj = _instanceObj; 

- (NSString*)instanceObj {
    return _instanceObj;
}

- (void)setInstanceObj:(NSString *)instanceObj {
    _instanceObj = instanceObj;
}
复制代码

@dynamic,意译动态的,@dynamic可使instanceObj编译不生成getter,setter方法,不生成实例变量。

@dynamic instanceObj;

所以@dynamic声明属性后,如直接调用getter、setter是会造成运行时的crash的。

属性特质

@property (nonatomic,readonly ,copy) NSString *someObject;

  • 原子性
    • 原子性(atomic):编译器所合成的方法会通过锁定机制确保其原子性,意思就是访问时开同步锁保证其多线程同时设置/访问时的数据安全。
    • 非原子性(nonatomic)非线程安全,一般情况下非特殊要求安全性的,都建义使用此修饰,因为没有同步锁,性读取能高。
  • 读写权限
    • readwirte可读写,意思是同时拥有getter,setter方法,其实就是@synthesize实现的。
    • readonly只读,意思是只有getter方法,没有实现setter方法。可以能过分类(category)的方式实现属性的改写。
  • 内存管理语义
    • assign(赋值引用)常用于基本数值类型的赋值操作,也可用于对象Object,不会使retainCount + 1,但对象释放时不会清空指针,容易造成野指针。
    • strong(强引用)表明该属性定义了一种『拥有关系』,该属性在设置新值时,会先保留新值,释放旧值,再赋值新值上去。在OC里面只要一个对象被一个强类型的指针引用,该对象就不会被释放,反之没有强类型的指针引用,该对象会被释放掉。
    • weak(弱引用/归零引用)表明该属性定义了一种『非拥有关系』,该属性在设置新值时,既不保留新值也不释放旧值,跟assign一样是简单的赋值,但weak是用于对象类型的,nt,属性值也会置nil,但assign修饰对象时不会置nil。
    • unsafe_unreatin 与assign相同,一般情况下assign是用于基本数值类型,unsafe_unreatin用于对象类型『非拥有关系』,当属性所属的对象释放时,不会清空指针,容易造成野指针,所以它是unsafe。
    • copy 与strong类似,该属性在赋值时,不会保留新值,而是copy一份,再赋值copy的内容给新值。好比我们在修饰NSString的时候一般都会用copy而不用strong,原因是当设置新值的时候,传过来的值是一个可变的字符串NSMutableString的时候使用strong强引用的话,那么传过来的这个新值NSMutableString有变动依然会改动属性的值,这不是安全的做法。所以使用copy,拷贝一份不可变的值是安全的做法。
    • setter = < name > 或者 getter = < name > 改变getter,setter名字的做法,一般情况不推荐使用。

第7条:对象内部尽量直接访问实例变量

  • 实例变量访问方式
    • 直接访问,如:_object
      • 不需要调用setter/getter,绕过其语义,速度快;
    • 通过属性访问,如:self.object,[self setObject:value]
      • 需要调用getter/setter方法,不绕过内存管理语;
      • 可触发KVO;
      • 可通过属性排查相关的错误,如传值的问题,打断点调试。 作者推荐的做法是写入变量时,使用设置方法setter,访问变量时使用实例变量直接访问,如此一来既可提高读取效率,亦可惯彻setter带来的相关好处。 不过有两个特例:
  1. 初始化方法和dealloc方法中,需要直接访问实例变量来进行设置属性操作。因为如果在这里没有绕过set方法,就有可能触发其他不必要的操作。
  2. 惰性初始化(lazy initialization)的属性,必须通过属性来读取数据。因为惰性初始化是通过重写get方法来初始化实例变量的,如果不通过属性来读取该实例变量,那么这个实例变量就永远不会被初始化。

第8条:对象的等同性

平时的编码中,要比较两相同数值类型的变量,我们直接用==来判断这变量是否相等,但==用在两对象类型的时候,比较的却是指针,也就是对象所在的内存地址。==对比对象出来的结果有时候未必是我们想要的。应该使用NSObject协议里面的『isEqual』方法来判断两对象的等同性。

OC中的字符串有EqualToString:方法来比较。 数组有isEqualToArray:方法和字典也有isEqualToDictionary:方法来比较。

深度等同性判定,例如比较两个数组,除了使用isEqualToArray方法外,还有深度的判断方式:先判断数组的元素数量是否等同,对等位置对象是否相同,如果都相同,则两数组为等同。

第9条:类簇模式,隐藏实现细节

平时的开发中,如遇要绘制多种形状的需求,程序员立马脑暴一番,提手码一个Shape基类,然后再码Circle、Triangle、Square…等等的一些继承Shape的子类。这是正常的做法,没问题。但有可能需求中要求要绘制的图形很多,几十个甚至上百,该如何?按照逻辑我们依然要建立N个形状的子类。这时候,如果要绘制其中的一个形状了,我们需要在几十个甚至上百个子类中找到我们所需的子类,无疑这是比较低效率的做法。

那么『类簇模式』就是为了灵活地解决这个问题而存在的一种设计模式。把基类作为『抽象基类』以应对多个类,隐藏其背后的实现细节。什么意思呢?以刚才的绘制图形为例,在『抽象基类』中建立一个『工厂方法』,通此方法可以实现所有的子类的创建,而你要做的只需要做的是在调用工厂方法的时候,传入你需要创建子类的type,上代码:

  • 基类实现
typedef NS_ENUM(NSInteger,ShapeType) {
    ShapeTypeCircle,            //圆
    ShapeTypeTriangle,          //三角形
    ShapeTypeSquare             //四方形
};

@interface Shape : NSObject

//实现创建所有形状子类的工厂方法
+ (Shape *)shapeWithType:(ShapeType)type;
- (void)draw;

@end
复制代码
@implementation Shape

+ (Shape *)shapeWithType:(ShapeType)type {
    switch (type) {
        case ShapeTypeCircle:
            {
                return [Circle new];
            }
            break;
        case ShapeTypeTriangle:
        {
            return [Triangle new];
        }
            break;
        case ShapeTypeSquare:
        {
            return [Square new];
        }
            break;
        default:
            break;
    }
}

- (void)draw {
    //no do anything
}

@end
复制代码
  • 子类实现
@interface Circle : Shape
@end

@implementation Circle

-(void)draw {
    //具体画圆的逻辑代码
}

@end
复制代码

其实cocoa系统框架里面存在许多类簇,如UIButton,NSArray…,所以,我们平时要创建一个UIButton的时候,直接调用[UIButton buttonWithType:UIButtonTypeContactAdd]就可以了,很是方便。 当然,使用类簇还是有几点要注意的,书中作者提到:

  1. 类型的判断

  2. 子类应该继承自类簇中的抽象基类

  3. 子类应该定义自己的数据存储方法,因为抽象基类只是一个壳,不作任何的数据存储。

  4. 子类应该覆写超类指定要覆写的方法,意思添加子类的时候,要看超类的开发文档。

    关于第1点的类型判断,平时我们判断一对象的类型的做法是

    id person = [[Person alloc] init];
    
    if ([Person class] == [person class]) {
        NSLog(@"is equal");
    }
    复制代码

    但是在类簇中使用类型判断则需要注意,不能像上面这样写,如

    id shape = [Shape shapeWithType:ShapeTypeCircle];
    if ([shape class] == [Shape class]) {
        //here never run
    }
    复制代码

    应该这样判断:

    if ([shape isKindOfClass:[Shape class]]) {
        NSLog(@"here we go");
    }
    复制代码

第10条:关联对象

到这里就属于runtime的内容了。什么是关联对象,顾名思义即对象之间的关联。打个比方Cocoa Foundation框架里的NSString类,现在你想为这个类增加一个属性要怎么做?用Category?但类别只能添加方法,是不能添加属性的。这时候关联对象就派上用场了,使用运行时方法,动态关联对象以达到添加属性的效果。

先看看关联对象的3个使用方法:

   //为某个对象设置关联对象的值:
   objc_setAssociatedObject(id  _Nonnull object, const void * _Nonnull key, id  _Nullable value, objc_AssociationPolicy policy)
   /*object 关联的主对象,const void *key 常量指针作为key,value 被关联的对象,policy 存储策略,定义内存管理语义。*/
   2、根据key获取关联对象的值:
   objc_getAssociatedObject(id  _Nonnull object, const void * _Nonnull key)
   3、移除关联的值:
   objc_removeAssociatedObjects(id  _Nonnull object)
复制代码

具体的用法,用个例子来说明。平时的开发中,总离不开UIButton的使用,用户点击Button,Button事件触发执行事件方法。正常的代码实现是先创建UIButton的实例,设置button的相关属性,然后为button添加一个点击事件,并在@selector上指定方法名。当有点击行为发生时,则会跳到selector指定的方法去执行。但这过程是创建button与点击事件后的方法是分离开的,可读性有那么点不大直观。现在用关联对象可使代码看上去整体性会更强一些。

先建一个UIButton的类别(Category)

@interface UIButton (ClickRespone)

typedef void (^ClickRespone)(UIButton *sender);
//点击回调的方法
- (void)buttonClickRespone:(ClickRespone)respone;

@end

//.m文件
@implementation UIButton (ClickRespone)

- (void)buttonClickRespone:(ClickRespone)respone {
   //utt
   objc_setAssociatedObject(self, @selector(buttonClick), respone, OBJC_ASSOCIATION_COPY);
   //添加事件
   [self addTarget:self action:@selector(buttonClick) forControlEvents:UIControlEventTouchUpInside];
}

//点击事件
- (void)buttonClick {
  ClickRespone clickRespone =  objc_getAssociatedObject(self, @selector(buttonClick));
   if (clickRespone) { //如果实现了block则回应点击事件
       clickRespone(self);
   }
}
@end
复制代码

具体使用:

 UIButton *someBtn = [UIButton buttonWithType:UIButtonTypeCustom];
   /*
    .
    */
 [someBtn buttonClickRespone:^(UIButton *sender) {
    NSLog(@"someBtn click");
 }];
复制代码

看上去,好像是直观了一些。

第11条 objc_msgSend的作用

在iOS开发中,使用对象调用一个方法用术语来说就是『传递消息』。消息有名称『name』,有选择子『selector』,可接受参数,也可能有返回值,其实就是平时我们所说的方法啦。

但OC中,对某个对象传递消息时,就会使用动态绑定机制来决定具体要调用的方法。什么是动态绑定机制?OC的对象在收到消息之后,要调用哪个方法完全由运行时决定的,甚至可以在程序运行时改变。 给对象发消息:

id returnValue = [someObject messageName:parameter];
复制代码

someObject为接收者,messageName为选择子selector,编译器遇到类似的消息会将它转换成C语言的函数调用(在底层所有的OC的方法都是C语言函数),如下:

void objc_msgSend(id self, SEL cmd,…)
复制代码

这是参数个数可变的函数,能接两个及两个以上的参数,第一个为消息的接收者,第二个为选择子,后面那些就是消息中的参数了。最终编译器会将消息转化为OC的方法:

id returnValue = objc_msgSend(someObject,
                              @selector(messageName),
                              parameter);
复制代码

这个方法执行之后,先会去消息接收者所属类中搜索『方法列表』list of methods,如果找到与选择子名称相同的方法就跳转到其实现代码。如果找不到就顺着继承链向上一层一层地找到跳转为止。如果还是不能找到的话,那就执行『消息转发』。

这里有个小疑问是:是不是每次传递消息得了这么费劲,先找类中的list of methods,找不到再找继承链。。之类的。其实不是,每个类都有一块缓存,里面放着一张『快速映射表』。它的作用是objc_msgSend如果匹配到方法就会将其缓存到里面,那下次如果再次遇到同样的消息,执行就会快很多。

第12条 消息的转发

OC的消息传递的最长链:向对象传递消息—>『快速映射表』—>list of methods(当前类找不到顺着继承链向上)—>消息转发。那是消息转发是怎么的一回事?看图

其实消息转发分为两大步骤:

1、动态方法解析(dynamic method resolution)

消息转发时,会先询问接收者是否有动态添加方法以处理当前这个未知的选择子(seletor)。会根据情况执行以下两个方法:

//当选择子方法为对象方法时执行
+ (BOOL)resolveInstanceMethod:(SEL)sel;
//当选择子方法为类方法时执行
+ (BOOL)resolveClassMethod:(SEL)sel;
复制代码

用一个例子来解释『动态方法解析』是怎么一回事,如下:

@interface MessageTransmit : NSObject

@property (nonatomic ,copy)NSString *someMessage;

@end

//.m文件
@interface MessageTransmit()

//用来放属性值的数据区
@property (nonatomic ,strong) NSMutableDictionary *propertyStore;

@end

@implementation MessageTransmit

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

//设置@dynamic之后,编译不会生成someMessage的getter,setter方法
@dynamic someMessage;

//当运行时未能从cache、及类及类的继承链中的list of methods匹配到someMessage的getter,setter方法的时候,先会调用此方法进行动态方法解析。
+ (BOOL)resolveInstanceMethod:(SEL)sel { 
    //获取选择子方法名
    NSString *selectorString = NSStringFromSelector(sel);
    if ([selectorString hasPrefix:@"set"]) {        //如果选择子执行的方法里开始是set,那就证明这是个setter方法
        //添加setter方法
        class_addMethod(self, sel,(IMP)autoSomeMessageSetter,"v@:@");
    }else {
        //添加getter方法
        class_addMethod(self, sel, (IMP)autoSomeMessageGetter,"@@:");
    }
    return YES;
}

//Getter C方法的实现
id autoSomeMessageGetter(id self,SEL _cmd) {
    MessageTransmit *typeSelf = (MessageTransmit *)self;
    NSMutableDictionary *store = typeSelf.propertyStore;
    //key
    NSString *key = NSStringFromSelector(_cmd);
    return [store valueForKey:key];
}

//setter C方法的实现
void autoSomeMessageSetter(id self,SEL _cmd,id value) {
    MessageTransmit *typeSelf = (MessageTransmit *)self;
    NSMutableDictionary *store = typeSelf.propertyStore;
    
    //因为上面取key的是getter的方法名,所以set的时候key也要跟getter的key保持一致。
    NSString *keyString = NSStringFromSelector(_cmd);
    NSMutableString *key = [keyString mutableCopy];
    // 去掉:
    [key deleteCharactersInRange:NSMakeRange(key.length - 1, 1)];
    
    // 去掉set
    [key deleteCharactersInRange:NSMakeRange(0, 3)];
    
    // 取出与getter key相同的串
    NSString *lowercaseFirstChar = [[key substringToIndex:1] lowercaseString];
    [key replaceCharactersInRange:NSMakeRange(0, 1) withString:lowercaseFirstChar];
    
    if (value) {
        [store setObject:value forKey:key];
    } else {
        [store removeObjectForKey:key];
    }
} 
@end
复制代码

调用

MessageTransmit *msgTransmit = [[MessageTransmit alloc] init];
[msgTransmit setSomeMessage:@"set the someMessage succed."];
NSLog(@"someString is %@",msgTransmit.someMessage); 
//最终结果为打印:set the someMessage succed.
复制代码

2、完整的消息转发(full forwarding mechanism),而完整的消息转发又分两小点:

  • 2.1 备援接收者

    当『动态方法解析』resolveInstanceMethod或resolveClassMethod返回NO的时候,消息转发将继续向后进行,runtime会请接收者再看看还有其它对象能处理这个未知选择子,方法如下:

    //指定Target接收未知的选择子,进行消息转发
    - (id)forwardingTargetForSelector:(SEL)aSelector;
    复制代码

    如果还是没有对象可以处理当前的消息,forwardingTargetForSelector返回nil的话,接下来就会进行完整的消息转发。

  • 2.2 完成的消息转发

    在 forwardInvocation: 消息发送前,Runtime 系统会向对象发送methodSignatureForSelector: 消息,并取到返回的方法签名用于生成 NSInvocation 对象。所以重写 forwardInvocation: 的同时也要重写 methodSignatureForSelector: 方法,否则会抛异常。

    //为当前选择子的方法进行签名,只有签名的选择子才能进行后续的转发
    - (NSMethodSignature*)methodSignatureForSelector:(SEL)selector;
    //完成的消息转发
    - (void)forwardInvocation:(NSInvocation *)anInvocation;
    复制代码

    由上面的描述可看出,forwardingTargetForSelector以及forwardInvocation都可以进行消息的转发,它们的区别在于forwardingTargetForSelector只能指定一个对象进行转发,而forwardInvocation可以是多个对象的转发。 还有就是class_addMethod的参数“v@:@”是类型编码,具体解释请参考:类型编码

第13条:用『方法调配技术』调试『黑盒方法』

众所周知,Object C对象接收到消息之后,究竟要调用何种方法只能在运行时才能确定。即使指定了选择子(selector)的方法名,在运行时也可以改变其具体的调用方法,这功能听上去就相当的粗大了。但具体这样做有什么卵用?意思是我们要改变一个类的功能就不需要拿到类的源码然后改写功能的本身,亦不需要继承其为子类重写功能了。而且这种改写后的新功能在类的所有实例用都有效,这种方案,专业上称之为『方法调配method swizzling』,江湖也有种叫法,叫什么『黑魔法』。其中到底是怎么的一回事? 类的方法列表会把选择子的名称映射到方法实现之上,这样一来『动态消息派发系统』就可以根据选择子的名字,找到要调用的方法了。这些方法都是以函数指针的形式来表示,而且这类的指针有个专业名字就『IMP』,其原型:

id (*IMP)(id,SEL,...)

说白了,IMP即方法的内存地址,能过IMP就能调用方法。

作者以NSString为例,NSString类可以响应lowercaseString、uppercaseString、capitalizedString等选择子。这张映射表中的每个选择子都映射到了不同的IMP之上,如下图:

Object C的运行时提供了一些方法,用来操作上面这个表。如新增选择子,改变选择子方法的实现,还可以交换两个选择子所映射到的指针(IMP),比如这样:

交互方法具体的实现,涉及到两个方法:

method_exchangeImplementations(Method method1, Method method2);
复制代码

上述方法,不是太傻应该能看出来,这是用来交换两方法的实现的。

class_getInstanceMethod(Class class, SEL selector);
复制代码

而上面这个是获取实例的方法的,也就是返回method_exchangeImplementations的参数Method.

具体实现代码:

@interface NSString (NCYAddition)

- (NSString *)ncy_lowercaseStrig

@end

@implementation NSString (NCYAddition)

- (NSString *)ncy_lowercaseStrig {
    NSString *lowercase = [self ncy_lowercaseStrig];//运行期会将ncy_lowercaseStrig对应于lowercaseString的方法的实现,所以这里是不会产生递归的死循环
    NSLog(@"%@ => %@", self, lowercase);//输出语句,便于调试
    return lowercase;
}

@end

//具体调用
Method lowercaseMethod = class_getInstanceMethod([NSString class], @selector(lowercaseString));
Method ncyLowercaseMethod = class_getInstanceMethod([NSString class], @selector(ncy_lowercaseStrig));
method_exchangeImplementations(lowercaseMethod, ncyLowercaseMethod);
    
NSString *testString = @"Hello, You Little shiT."; 
复制代码

打印的结果为:『Hello, You Little shiT. => hello, you little shit』

最后作者有提到,此技术应该用于调试程序的时候使用,滥用之,会使代码可读性差且难以维护。

第14条:理解『类对象』的用意

Object C的对象本质是指向某块内存数据的指针。例如说:

NSString *someStr = @"some string";
复制代码

someStr前面有个*号,说明它是个指定,而且这个指针指向内存中存放some string字符串的区域。someStr本质是一个内存地址,它标志内存中某块数据。Object C中所有的对象都是如此。 id是能用的对象类型,但由于它自己本身就已经是指针,所以在使用它接收对象的时候,能够:

id someStringObj = @"some string";

对比上下两种方式声明字符串语法意义上是相同的,区别在于使用具体类型时,对象在调用其类型没有的方法时,编译是会报错的。而使用id则不会,它会在运行时报错。

id 在运行时库里面的定义是:

typedef struct objc_object { 
    Class isa
} *id
复制代码

id是一个isa指针,什么是isa指针,稍后讲到。再看看Class对象在运行期库里的定义:

struct objc_class {
    Class _Nonnull isa  OBJC_ISA_AVAILABILITY;
    
#if !__OBJC2__
    Class _Nullable super_class                              OBJC2_UNAVAILABLE;
    const char * _Nonnull name                               OBJC2_UNAVAILABLE;
    long version                                             OBJC2_UNAVAILABLE;
    long info                                                OBJC2_UNAVAILABLE;
    long instance_size                                       OBJC2_UNAVAILABLE;
    struct objc_ivar_list * _Nullable ivars                  OBJC2_UNAVAILABLE;
    struct objc_method_list * _Nullable * _Nullable methodLists                    OBJC2_UNAVAILABLE;
    struct objc_cache * _Nonnull cache                       OBJC2_UNAVAILABLE;
    struct objc_protocol_list * _Nullable protocols          OBJC2_UNAVAILABLE;
#endif

} OBJC2_UNAVAILABLE;
复制代码

Class中的首个变量也是isa指针,isa指针它其实是指定『元类meta class』的,意思是当前对象所属的类型。前面的id someStringObj = @"some string";是一个(is a)NSString,所以isa指针就指向了NSString。那Class的定义里面也有isa指针,也表明Class其实也是一个Object C的对象,它也有自己的所属类型。 结构体中的super class是定义超类的,确定继承关系。name为类型的名称const不可变。ivars类的成员列表。methodslist方法列表等等。

假如你定义了一个类名为SomeClass,SomeClass的子类从NSObject中继承而来,那其继承体系图如下:

在继承体系中查询类型信息

isMemberOfClass :判断是否是某个类的实例; isMemberOfClass :判断对象是否为某个类或派生类的实例;

NSMutableDictionary *dict = [NSMutableDictionary new];  
[dict isMemberOfClass:[NSDictionary class]]; ///< NO 
[dict isMemberOfClass:[NSMutableDictionary class]]; ///< YES 
[dict isKindOfClass:[NSDictionary class]]; ///< YES 
[dict isKindOfClass:[NSArray class]]; ///< NO 
复制代码
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值