iOS-有效编写高质量Objective-C方法-三

1,653 阅读12分钟

欢迎大家关注我的公众号,我会定期分享一些我在项目中遇到问题的解决办法和一些iOS实用的技巧,现阶段主要是整理出一些基础的知识记录下来

文章也会同步更新到我的博客:
ppsheep.com

本篇文章,主要是对OC中的一些原理的讲解,可能会有一些枯燥,但是真正当你理解时,会有一种豁然开朗的感觉。这里会涉及到 对象,属性,消息以及运行期的一些介绍,只有我们真正理解了这些原理之后,我们的开发水平才会进一步提升,而不是止步于view的简单编写,view的编写想要写得好,也需要了解这一些原理。

理解属性(property)这一概念

在开始讲属性之前,我们先来理解一下几个概念:

  • 对象:对象是"基本的构造单元",在OC中,我们经常使用对象来储存和传递数据
  • 消息传递:在对象之间传递数据并执行任务的过程就叫做消息传递
  • OC Runtime:当应用程序运行起来,为其提供相关支持的代码叫做"运行环境(Runtime)",它提供了一些使得对象之间能够传递数据的函数,并且包含了创建类实例需要的所有逻辑

上面三个概念,在OC编程中尤其重要,虽然现在可能你没有很深刻的理解,但是随着学习深入,你肯定能够体会到。

属性

"属性(property)"相信大家都很熟悉,是OC中用来存储对象的数据的实例变量。实例变量一般是通过"存取方法"来访问,"设置方法"来设置实例变量。在之前我们已经讲过,对于实例变量,如果是本身访问,那么读取最好是直接读取(采用下划线方式直接访问),而设置最好使用属性来设置。具体的,可以参见上一篇(iOS-有效编写高质量Objective-C方法-二)。

对于一些简单的概念性的东西我就不讲了,给出结论就行:

  • 不一定要在接口中或者说是声明文件中定义好所有的实例变量,可以在实现文件中定义,以保护与类实现相关的内部信息
  • 属性按照标准的命名方式,在编译器编译期间会自动加上存取方法

接下来,我们来说几个关键字:

@synthesize

我们可以在代码中通过这个关键字来指定我们想要的实例变量

例如:在头文件中

@interface : NSObject

@property(nonatomic, copy) NSString *name;

@end

这个属性,在我们运行期环境下,生成的实例变量为_name,但是我们在.m中并不想使用这个名称,那么我们在实现文件里就可以这样写:

@implementation

@synthesize name =  _myName

@end

那么我们在.m实现文件中,都可以直接使用_myName来操作属性name

不过为了书写的规范,和团队之间协作,我还是建议按照规范的OC代码风格来编写代码,团队成员之间,一看就能够看清楚代码

@dynamic

这个关键字 是用来阻止编译器自动合成存取方法,不过这个关键字我都用的很少,上面的关键字同样的,也使用较少。

这个关键字的意思是:阻止编译器合成属性所对应的实例变量,也不要合成实例变量的存取方法

这里讲一下实例变量就是带下划线的_name而属性是通过property声明的name

这两个需要区分开来

属性特质

nonatomic

所谓的属性特质,就是指我们在申明属性的时候,property括号中跟的一些关键字

@property(nonatomic, readwrite, copy);

其中的nonatomic,readwrite,copy这些都是属性特质,我们先来说nonatomic

这个关键字叫做属性的原子性,通俗来说,这个关键字主要是来控制属性的同步锁
同步锁:不同的线程在读取属性的时候,如果属性是通过atomic来声明的,那么这两个线程总是能够读到属性的有效值(注意这里是有效的属性值,并没有说是正确的属性值),如果属性是通过nonatomic声明的,那么不同的线程读取属性值时,如果有线程正在修改该属性的值,另外的线程正在读取属性值,那么就可能将还未修改完成的属性值读取出来(这里是尚未修改完成的属性值,有可能读出一个完全没有任何意义的属性值)

那么又要来说一说,为什么我们总是看到在编写iOS程序时,属性总是使用nonatomic来声明的呢,这是因为在iOS中使用同步锁的开销太大,这会带来性能问题。在一般情况下,我们并不要求属性必须具有原子性,因为这个原子性并不是说就是"线程安全了",如果我们需要实现线程安全,那么还需要使用更为底层的同步锁定机制才行,即便是使用atomic来声明,不同的线程还是可能读取到不同的属性值,只是说这个属性值是有效的,有意义的。

所以我们在开发iOS程序时,还是使用nonaomic来声明,但是在macOS程序开发中,却不会遇到这种性能瓶颈,性能配置不一样嘛

readwrite/readonly

这个属性特质,我们根据字面意思就能看出来,就是声明属性权限的,这个也没什么好说的了。

strong/copy/assign/weak

这个可能是我们平时用的最多的,也是思考最多的,其实平时我们怎么用,都是知道的,但是为什么这么用呢?

assign:"设置方法"只会针对"纯量类型"进行赋值,例如CGFloat、NSInteger这种

strong:此特质象征了一种拥有关系,在"设置方法"中,这种属性是先保留新值,并且释放旧值,然后将新值设置上去

copy:这种方法和strong类型有点相似,但是它并不是保留新值,而是直接就想新值拷贝,当属性为NSString类型时,我们经常使用这种,那么为什么我们在NSString经常使用拷贝呢,以为我们在设置时,可能会传进来一个NSMutableString对象,这个对象是NSString的子类,是可以赋值给NSString的,如果我们不使用拷贝,那么当外部改变NSMultableString值时,我们的属性值也会直接被修改掉,所以这时,我们就需要拷贝一份不可变的

weak:这个是一种弱引用,为这种方法设置时,既不会保留新值,也不会释放旧值,当属性所指的对象销毁时,属性值也会被清空,在ViewController中定义view时我们经常会使用到weak,但是我们经常还是将view声明为strong,当然这使用起来不会有很大的影响,但是我们的应用在运行过程中,就会产生很多的没用view属性值没有被释放掉,占用无效内存。所以建议大家在使用view时,还是声明为weak

@property(nonatomic,weak) UILable *lable;

//初始化lable时

UILable * lable = [[UILable alloc] init];
[self.view addSubview: lable];
self.lable = lable;

方法名

在我们定义的属性为Boolean值时,我们的习惯是获取方法,一般是"is"
开头,那么我们就可以在声明时,这样书写

@property(nonatomic,getter=isOn) Bool on;

属性的获取方法,就成了isOn;

我们在属性中定义了属性特质,那么我们在书写赋值时,就应该严格按照属性特质赋值,例如,我们有一个初始方法,需要对我们的属性NSString name赋值

- (instancetype)initWithName:(NSString *)name{
    if(self = [super init]){
    //此处就应该使用copy来对name赋值
        _name = [name copy];
    }
}

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

类族是一种隐藏抽象基类背后实现细节的很有用的模式。。那么什么叫做类族呢?

举个例子:

在UIKit中有一个名叫UIButton的类,如果想要创建按钮,我们可以调用一个类方法

+ (UIButton *)buttonWithType:(UIButtonType)type;

该方法返回的对象,取决于传入按钮的类型,然而,不管传入的是什么类型,返回的类都是继承自同一个基类:UIButton。 这样,所有继承自UIButton的类组成了一个类族。

在系统框架中,使用到了很多的类族。。

那么为什么要这样做呢?
UIButton这个例子,在实现时,使用者无需关心创建出来的按钮是属于哪一个类,也不用考虑按钮的绘制细节,我只需要知道,我怎么创建按钮,如何设置标题。如何增加点击操作等。

创建类族

我们现在来假设一个处理雇员的类,每个雇员都拥有自己的名字和薪水这两个属性,管理者可以命令器执行日常工作。但是,各种雇员的工作内容却不同,经理在带领雇员做项目的时候,无需关心每个人怎样完成自己的工作,只需要指示其开工即可。

首先我们定义一个基类:

typedef NS_ENUM(NSUinteger, PPSEmployeeType){
    PPSEmployeeTypeDeveloper,
    PPSEmployeeTypeDesigner,
    PPSEmployeeTypeFinance,
}

@interface PPSEmployee    :    NSObject

@property (nonatomic, copy) NSStirng *name;
@property (nonamotic, assign) NSUInteger salary;

//创建方法
+ (PPSEmployee *)employeeWithType:(PPSEmployeeType) type;

//指示开工
- (void)doADaysWork;

@end
@implementation PPSEmployee

+ (PPSEmployee *)employeeWithType:(PPSEmployeeType) type{
    switch (type){
        case PPSEmployeeTypeDeveloper:
            return [PPSEmployeeDeveloper new];
            break;
        case PPSEmployeeTypeDesigner:
            return [PPSEmployeeDesigner new];
            break;
        case PPSEmployeeTypeFinance:
            return [PPSEmployeeFinance new];
            break;
    }
}

- (void)doADaysWork{
    // 子类覆写
}

@end

每个实体子类都是从基类继承而来

@interface PPSEmployeeDeveloper    :    PPSEmployee
@end
@implementation PPSEmployeeDeveloper

- (void)doADaysWork{
    [self coding];
}

@end

我们上面实现的方式,是根据雇员的类别,创建雇员类的实例,这其实是一种工厂模式。在Java中,我们知道这种方式一般是通过抽象类来实现,但是OC中没有抽象类这一说,于是开发者通常会在文档中写明使用方法。

在Cocoa中 有很多的类族 大部分的集合类型都是类族 我们在一个对象是否是属于某一个类时,如果我们采用下面的方式,往往得不到我们想要的效果:

id myArr = @[@"a",@"b"];
if([myArr class] == [NSArray class]){
    //这里永远不会跑到 因为 我们知道这里[myArr class]返回的永远是NSArr的一个子类,NSArray只是一个基类
}

当然,我们要做这个判断的时候,应该都知道是使用isKindOfClass 这个方法,这个方法其实是用来判断是否是同一类族,而不是某个类

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

有时候我们需要在对象中存放相关的信息,这时候我们能想到的方法就是,创建一个子类,然后我们使用的时候 直接使用子类。 但是并不是所有情况都能够这样做,有时候类的实例可能是由于某种机制所建立的,我们开发者是没有办法创建出自己建立的子类的实例。幸好,我们可以通过OC的一项强大的特性"关联对象"来解决这个问题。

那么什么事关联对象呢?

我们可以给某个对象关联许多其他对象,这些对象通过"键"来区分。存储值的时候,可以通过指明存储策略来维护内存,存储策略就是你存储的是一个NSString啊,那你就该把从存储策略改为copy,类似于这种

下面是对象关联类型:

关联类型 等效的@property属性
OBJC_ASSOCIATION_ASSIGN assign
OBJC_ASSOCIATION_COPY_NONATOMIC nonatomic,copy
OBJC_ASSOCIATION_RETAIN_NONATOMIC nonatomic,retain
OBJC_ASSOCIATION_COPY copy
OBJC_ASSOCIATION_RETAIN retain

管理关联对象的相关方法:

  • void objc _ setAssociatedObject(id object, void *key, id value, objc_AssociationPolicy policy) 使用此方法 以给定的键和策略为某对象设置关联对象
  • void objc _ getAssociatedObject(id object, void *key) 使用此方法根据给定的键从某对象中获取相关的关联对象的值
  • void objc_removeAssociatedObject(id object)使用此方法移除指定对象的全部关联对象

使用这种方法时,我们可以把某对象想象成一个NSDictionary,把关联到该对象的值理解为字典中的条目,那么这些关联对象,就相当于设置字典里的值和获取字典里的值了

使用举例

在iOS中,我们如果想要使用UIAlertView 我们需要这样定义

 UIAlertView *alert = [[UIAlertView alloc] initWithTitle:@"你确定吗?" 
                                     message:@"可能没那么确定吧" 
                                     delegate:self
                                     cancelButtonTitle:@"取消" 
                                     otherButtonTitles:@"继续", nil];
 [alert show];

然后我们需要实现UIAlertView的代理,来进行操作的识别判断


-(void)alertView:(UIAlertView *)alertView clickedButtonAtIndex:(NSInteger)buttonIndex{
    if (buttonIndex == 0) {
        [self doCancle];
    }else{
        [self doContinue];
    }
}

这样写,现在看来是没有什么问题的,但是如果我们需要在当前的一个类中,处理多个警告信息,那么代码将会变得复杂,我们需要在delegate中判断当前的UIAlertView的信息,根据不同的信息实行不同的操作。

如果 我们能在创建UIAlertView的时候 就将这些操作做好,那么,在delegate中我们就不需要判断UIAlertView了。事实上这种方法是可行的。

UIAlertView *alert = [[UIAlertView alloc] initWithTitle:@"你确定吗?" 
                                 message:@"可能没那么确定吧" 
                                 delegate:self 
                                 cancelButtonTitle:@"取消" 
                                 otherButtonTitles:@"继续", nil];
void (^block) (NSInteger) = ^(NSInteger buttonIndex){
    if (buttonIndex == 0) {
        NSLog(@"cancle");
    }else{
        NSLog(@"continue");
    }
};

//将block设置为UIAlertView的关联对象
objc_setAssociatedObject(alert, PPSMyAlertViewKey, block, OBJC_ASSOCIATION_COPY);
[alert show];

我们只需要在delegate中拿到block就行

 void (^block)(NSInteger) =  objc_getAssociatedObject(alertView, PPSMyAlertViewKey);
 block(buttonIndex);