iOS 关键字全面详解

462 阅读11分钟

按照使用场景和作用可分为六个大类:

  • 修饰器类型:@property、@synthesize、@dynamic
  • 原子安全类型: nonatomic、atomic
  • 计数引用类型: strong、weak、assign、copy、retain、unsafe_unretained
  • 读写类型:readonly,readwrite
  • 访问类型:extern、static、const
  • 局部引用类型:__weak、__block、__strong

1、修饰器类型:@property、@synthesize、@dynamic

其实我也不清楚这种@修饰写法怎么称呼才好,但是我觉得这个写法很像Python里面的修饰器语法,所以我这暂且称之为修饰器类型

1.1、@property

@property (nonatomic,copy) NSString *str;

这么声明属性,相当于声明了成员_str,并默认实现了setStr和getStr方法(前提是我们没有自己写setter和getter)

1.2、@synthesize

#import "ClassA.h"

@interface ClassA()

@property NSString * str3;

@end

@implementation ClassA
// 或者写成 @synthesize str3
@synthesize str3 = str4;

@end

@synthesize要用的话,必须得声明了同名的属性,例如我已经声明了str3,再用@synthesize声明str3,那么就相当于编译器默认生成了一个str3属性和一个str3成员变量。如果再对@synthesize声明的str3赋值str4,那么就相当于生成了str4成员变量,str3的getter个setter实际上操作的都是str4.上面的代码等价于下面的代码.(成员变量和成员属性的区别可以看这里

#import "ClassA.h"

@interface ClassA(){
    NSString * str4;
}

@property (nonatomic,copy) NSString * str3;

@end

@implementation ClassA

-(NSString *)str3 {
    return str4;
}

-(void)setStr3:(NSString *)str3 {
    str4 = str3;
}

@end

1.3、@dynamic

@dynamic声明属性就相当于告诉编译器,该属性的getter和setter方法由我自己生成,你不要给我报错。一般可通过Category添加getter和setter,或者用runtime的class_addMethod添加方法。感觉用的很少,很少见,作了解就行。

2、原子安全类型: nonatomic、atomic

这两个类型就是根线程相关的,atomic与nonatomicd的主要区别就是系统自动生成的getter/setter方法不一样。

2.1、nonatomic

nonatomic表示非原子属性,也就是线程不安全类型,系统自动生成的getter/setter方法不会进行加锁操作。其内部的具体实现如下:

@property (nonatomic,strong) UIImage *image0;
@synthesize image0 = _image0;
/// MRC模式下

- (UIImage *)image0{
    return _image0;
}

- (void)setImage0:(UIImage *)image0 {
    if (_image0 != image0) {
        [_image0 release];
        _image0 = [[image0 retain] autorelease];
    }
}

2.2、atomic

atomic表示原子属性,表示线程安全,系统自动生成的getter/setter方法会进行加锁操作。其内部的具体实现如下:

@property (atomic,strong) UIImage *image1;
@synthesize image1 = _image1;
/// MRC模式下

- (UIImage *)image1 {
    @synchronized (self) {
        return _image1;
    }
}

- (void)setImage1:(UIImage *)image1 {
    @synchronized (self) {
        if (_image1 != image1) {
            [_image1 release];
            _image1 = [[image1 retain]autorelease];
        }
    }
}

那么用atomic修饰的属性也不一定就完全是线程安全的,比如我们定义一个atomic修饰的NSMutableArray属性,我们在执行remove或者add的时候,此时就不是线程安全的,因为remove和add不是原子操作。线程安全的根本是getter和setter方法的线程安全。

注意:atomic会比nonatomic更消耗性能,线程不安全的类型例如NSMutableArray等,就直接用atomic,线程安全类型的也少用atomic,实在要控制线程安全可通过代码加锁来处理。

3、计数引用类型: strong、weak、assign、copy、retain、unsafe_unretained

3.1、strong

strong一般用于修饰类类型,表示指向该对象并拥有该对象,比如修饰NSObject、UIImage等。属于强引用,也可以称之为浅拷贝(指针拷贝),相当于在该属性赋值的时候,只是将指针指向的地址复制给了另一个指针,所以不管多少次赋值,这些指针也指向的同一个对象。

3.2、weak

weak一般是用于修饰防止循环引用的对象,例如delegate(在MRC下delegate使用assign修饰)。如果我们用来修饰比如NSURL、NSObject类型的时候,就会因为没有强指针指向,在作用域结束的时候被释放。

当一个对象不再有strong类型的指针指向它的时候,它就会被释放,即使还有weak型指针指向它,那么这些weak型指针也将被清除。

之前还看过一个问题:weak属性修饰的变量,如何实现在变量没有强引用后自动置为 nil ?runtime 维护了一个weak_table_t 弱引用表 ,用于存储指向某一个对象的所有weak指针。weak表其实是一个哈希表,key是所指对象的地址,value是weak指针的地址的数组。在对象回收的时候,根据对象的地址将所有weak指针地址的数组,遍历数组把其中的数据置为nil

3.3、assign

assign一般用于修饰基本数据类型,例如NSInteger、BOOL、int、double、id等。assign修饰的属性,会被分配到栈上,栈的内存会由系统统一管理,不会造成野指针。当然如果是由assign来修饰对象类型,该对象被释放的时候,但是指针还是指向对象之前的地址,就变成了野指针(如果用了assign来修饰对象,在dealloc函数中将该对象指针手动置为nil,也可以消除野指针)。

前文中我提到了,在MRC下delegate使用assign修饰。然后也介绍了weak修饰的对象被释放时,指针会主动置为nil。然后就产生了一个疑问,为啥MRC模式下不用weak来修饰??然后我在代码中试了一下,如果在MRC模式下用weak修饰对象,就会造成每访问一次,引用计数就会自增1,这样会导致该对象无法被释放,如果不把对象置为nil,就会内存泄露。

3.4、copy

同样用于修饰OC对象类型的数据,同时在MRC即(MMR)手动内存管理时期,用来修饰block,因为block需要从栈区copy到堆区,在现在的ARC时代,系统自动给我们做了这个操作,所一现在使用strong或者copy来修饰block都是可以的。copy和strong相同点在于都是属于强引用,都会是属性的计数加一,但是copy和strong不同点在于,它所修饰的属性当引用一个属性值时,是内存拷贝(深拷贝),就是在引用是,会生成一个新的内存地址和指针地址来,和引用对象完全没有相同点,因此它不会因为引用属性的变更而改变。一般用于修饰类似NSString、NSDictionary等

上面提到了block,如果用copy或者strong修饰的话,block会从栈拷贝到堆区。那么如果block在栈区和堆区有什么区别吗?具体的可参见这里

还有就是copy用来修饰NSMutableArray、NSMutableDictionary、NSMutableString这种不可变对象会导致问题,初始化之后实际生成的却是该类型对应的不可变对象,例如NSArray、NSDictionary、NSString

3.5、retain

retain主要用在MRC模式下,作用和strong几乎等同。

但是,还有一点区别。在MRC模式下,修饰block时,strong相当于copy。修饰block时,retain相当于assign。导致结果:如果用strong修饰没有问题,如果用retain修饰会崩溃。报野指针错误。

3.6、unsafe_unretained

unsafe_unretained类型指针指向一块内存时,内存的引用计数也不会增加,这一点与weak一致。但是与weak类型不同的是,当其所指向的内存被销毁时,unsafe_unretained类型的指针并不会被赋值为nil,也就是变成了一个野指针。对野指针指向的内存进行读写,程序就会crash。(直接拷贝的别人写的,没用过,我试了一下别人的demo好像也没因为野指针crash,建议还是少用这个吧,weak还是挺香的)

4、读写类型:readonly,readwrite

4.1、readonly

readonly相当于是系统只会默认生成该属性的getter方法,只可读不可写,用点属性的形式去赋值,那么就会编译报错。

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

WeChat4faf1fc208c2cd9bf50486b0216219b4.png

但是,这并不意味着这个属性就不能赋值。可以有两种方法来强势给该属性赋值

方案一:如果是在该类的外部访问readonly属性,利用kvc直接赋值

WeChat989382a401cb7c15680c00ec7810881b.png

方案二:如果是在该类的内部访问,直接按照getter的规范,把getter方法补上就能可读可写。

通常情况下试用readonly这个修饰,已经能保证类内部的实现的安全了,如果要完全杜绝外界使用kvc来访问,可以通过在类的内部重写accessInstanceVariablesDirectly,固定返回No,那么外界就无法再通过kvc形式来更改

+(BOOL)accessInstanceVariablesDirectly {
    return NO;
}

4.2、readwrite

readwrite就是声明该属性可读可写,一般默认就是readwrite,可以不用额外声明。

5、访问类型:extern、static、const

5.1、extern

用法一:首先在.h文件中声明如
#import <UIKit/UIKit.h>

extern NSString * testStr;

@interface FromView : UIView

@end

然后在.m文件中赋值

#import "FromView.h"

NSString *testStr = @"testStr";

@implementation FromView

@end

然后我们在其他文件中引入该文件的.h之后就能访问我们定义的testStr,需要注意的是,一定分成两步来写,定义同时并赋值会报错。

用法二:用于访问全局变量

可以使用extern关键字访问全局变量,前提是该全局变量没有static关键字修饰

我们先定义了 NSString *globalStr = @"helloworld";,然后再别的文件中可访问该变量

extern NSString *globalStr;
globalStr = @"hello world";

5.2、static

static修饰的称之为静态变量,如果配合上const使用,就是静态常量。

  1. static关键字修饰局部变量,可延长局部变量的生命周期,直到程序结束运行为止。
  2. static关键字修饰局部变量,不会改变局部变量的作用域,该局部变量还是只能在当前作用域范围内使用。
  3. static关键字修饰(在.m中声明)的全局变量,使其只在本.m文件内部有效,而其他文件不可连接或引用该变量(即使在外部使用extern关键字也无法访问)。

被static修饰的变量仅在 < 编译阶段 > 初始化一次,在 < 全局 / 静态区 > 为它分配一份内存,一直到程序结束运行由系统回收。

5.3、const

const用于修饰指针变量和基本数据类型,const放在谁前面,谁就是常量不可再更改,下面代码演示:

WeChatc4b75d085488000d80e15778a0894312.png

6、局部引用类型:__weak、__block、__strong

6.1、__weak

有时在使用block 的时候,由于self 是被强引用的,在 ARC 下,当编译器自动将代码中的block从栈拷贝到堆时,block 会强引用和持有self,而 self 恰好也强引用和持有了 block,就造成了传说中的循环引用。 此时 __weak 就出场了,在变量声明时用 __weak修饰符修饰变量 self,让 block 不强引用 self,从而破除循环。

__weak __typeof(self) weakSelf = self; 
self.testBlock = ^{
       [weakSelf test];
});

ps: 弱引用不会影响对象的释放,但是当对象被释放时,所有指向它的弱引用都会自定被置为 nil,这样可以防止野指针。

6.2、__block

在一个 block里头如果使用了在 block之外的变数,会将这份变数先复制一份再使用,也就是,在没有特别宣告下,对于目前的block 来说,所有的外部的变数都是只可读的,不可改的。 如果我们想让某个 block 可以改动某个外部的变数,我们则需要将这个可以被 block 改动的变数前面加上__block。从另一个角度说,使用了__block关键字的变量会将变量从栈上复制到堆上。栈上那个变量会指向复制到堆上的变量。

__block int i = 1;
void (^block)(void) = ^{
     i = i + 1;
};

6.3、__strong

在上述使用 block中,虽说使用__weak,但是此处会有一个隐患,你不知道 self 什么时候会被释放,为了保证在block内不会被释放,我们添加__strong。

__weak __typeof(self) weakSelf = self; 
self.testBlock =  ^{
    __strong __typeof(weakSelf) strongSelf = weakSelf;
    [strongSelf test]; 
});

PS: __strong 表示强引用,对应定义 property 时用到的 strong。当对象没有任何一个强引用指向它时,它才会被释放。

weakSelf是为了block不持有self,避免循环引用,而再声明一个strongSelf是因为一旦进入block执行,就不允许self在这个执行过程中释放。block执行完后这个strongSelf会自动释放,没有循环引用问题。

比较__block & __weak
1. __block不管是ARC还是MRC模式下都可以使用,可以修饰对象,还可以修饰基本数据类型。
2. __weak只能在ARC模式下使用,也只能修饰对象(NSString),不能修饰基本数据类型(int),__strong也是一样的。
3. __block对象可以在block中被重新赋值,__weak不可以。
4. __block对象在ARC下可能会导致循环引用,非ARC下会避免循环引用,__weak只在ARC下使用,可以避免循环引用。

如果各位大佬看完觉得有哪里写的不对或需要补充的,请留言指出

参考文章: