iOS中的常用关键字

strong copy weak assign __block __weak __unsafe_unretained __strong atomic nonatomic synthesize dynamic synchronize auto static ...这些关键字在平时的开发中都经常用到,面试也是常问的,想来答案也都是知道的,但如果说到原理和底层的话,就涉及到很多的知识了。

还有一些swift中的关键字,之后会一起加进来。

1.MRC内存管理

上面的很多关键字是用来修饰属性的。其功能是在属性的setter方法里起到了作用。OC根据引用计数器"retainCount"来管理对象的内存,如果retainCount=0对象就会被销毁。MRC时内存管理的本质就是维持retain和release的平衡,对象才能正常被销毁。

  • 设置内存管理为MRC


    设置内存管理为MRC.jpg
  • 开启僵尸对象检测内存泄漏


    开启僵尸对象检测内存泄漏.jpg
- (void)setStudent:(Student *)student {
    if(_student != student) {
        [_student release];
        _student = [student retain];
    }
}

- (void)dealloc {
    self.student = nil;  
    [super dealloc];
}
Student *student = [[Student alloc]init]; //retainCount=1
Teacher *teacher = [[Teacher alloc]init];
teacher.student = student; //retainCount=2
    
[student release]; //retainCount = 1;
[teacher release]; //retainCount = 0;

👆这段代码,如果在set方法里什么都不干的话,那么就不能保证只要teacher活着,student就不销毁。解决的方案就是在set方法里,对student的引用计数器进行+1,并且当teacher销毁时要进行减1的操作。这样看来是十分简单的。但如果有两个student对象,并且teacher刚开始持有student1然后又持有student2,最后当teacher销毁的时候,只对_student进行了减1的操作,也就是student2会被销毁,而student1不会被销毁,这样就会造成内存泄漏。

Student *student1 = [[Student alloc]init];
Student *student2 = [[Student alloc]init]; 
    
Teacher *teacher = [[Teacher alloc]init];
teacher.student = student1; 
teacher.student = student2; 

[student1 release]; 
[student2 release]; 
[teacher release]; 

2.深拷贝和浅拷贝

拷贝:在iOS中,拷贝的目的,就是产生一个副本,就如同我们平时拷贝一个文件一样的。副本和源对象之间互不影响,修改任何一方不会影响对方。判断是深拷贝还是浅拷贝就是判断有没有产生新的对象,如果产生了新的对象,那么就是深拷贝。如果没有产生新对象,就是浅拷贝。

copy: 不管源对象是否可变,返回的都是不可变对象
mutableCopy: 不管源对象是否可变,返回的都是可变对象

NSMutableString *str = [NSMutableString stringWithFormat:@"test"];
NSString *str = [[NSString alloc]initWithFormat:@"test"];
NSString *str1 = [str copy];
NSMutableString *str2 = [str mutableCopy];

👆不管str是NSMutableString还是NSString,str1都是不可变类型,str2都是可变类型。为了保证源对象和副本对象之间互不影响,所以当用mutableCopy的时候,怎么样都要用一个新的对象,才能保证互不影响。即mutableCopy是深拷贝,也可以说是内容拷贝。而copy,分为源对象是可变还是不可变。如果源对象是可变的对象,那么copy操作需要返回一个不可变的对象,为了保证互不影响,这个时候也需要产生一个新的对象,即深拷贝。如果源对象本身就是不可变的,而copy操作也是返回一个不可变对象,两个都不可变,就不需要产生一个新的对象了,这个时候copy就是浅拷贝,也是指针拷贝。

深拷贝: 产生新对象,源对象的引用计数没有改变。
浅拷贝: 没有产生新对象,源对象引用计数器+1。

只要用copy修饰的属性,肯定是不可变类型,如果用可变类型去接收,那么就可能出现crash。

@property(nonatomic,copy)NSmutableArray *array;

3.多线程和Block

这里只是简单的讲述。关于多线程安全,有时间单独写一篇。在开发中往往会出现多条线程访问同一个资源的问题,那么如果这多个线程都要对这个资源进行数据修改的时候,由于访问资源的时间不确定性就可能会导致数据错乱的问题。线程同步的方案是利用锁🔐的技术,保证多条线程按照顺序访问同一资源。

Block本质,见另一篇。

4.属性修饰关键字

assing
set方法不做内存管理处理,直接赋值。常用来修饰基本类型。
assign可以用来修饰对象吗?我感觉这个问题换成assign修饰对象时会出现什么问题更好一些。因为修饰词没有什么限制,只是根据修饰时会有什么影响决定修饰什么类型的变量。

@interface Teacher : NSObject
 @property (nonatomic, assign) Student *student;
@end

student变量用assign修饰

// student变量用assign修饰的setter和dealloc方法
- (void)setStudent:(Student *)student {
    _student = student;
}

- (void)dealloc {
    [super dealloc];
}

student变量用strong修饰: ARC会自动管理setter和dealloc方法的内存

// setter和dealloc
- (void)setStudent:(Student *)student {
    if(_student != student) {
        [_student release];
        _student = [student retain];
  }
}
- (void)dealloc {
    self.student = nil;
    [super dealloc];
}
灵魂画家之画.jpg

比较上面的代码和图片。当teacher释放的时候,2号线线是不会被置为nil的,就会造成野指针错误。如果用strong的话系统会在dealloc方法里将2号线置为nil。

retain
效果同上面👆set方法的内存管理实现。即把原来的对象进行一次release操作,新对象做一次retain操作。

copy
可以参考上面拷贝的本质。常用来修饰字符串。原因见👇

strong 与__strong
强引用对象,对象的引用计数器+1,和retain达到的效果是一样的。__strong一样原理。

weak 与__weak
指针指向对象,但不会强引用对象,即引用计数器不会+1。另外当weak指针指向的对象被销毁时,weak会被置为nil,所以weak是安全的。__weak原理一样。weak本质见👇 由于不会强引用对象,所以用__weak修饰的对象,在block内部也不会被强引用,所以能够解决block循环引用的问题。

__unsafe_unretained
和weak一样,不会强引用对象,但当对象销毁时,指针不会被置为nil,所以是不安全的。但__unsafe_unretained的性能比较高(__weak需要哈希查找)在明确对象的生命周期的情况下,可以用__unsafe_unretained。看起来__unsafe_unretained和assign差不多,都不会在对象销毁的时候被释放,那么它们有什么区别呢?

synthesize
早期时,写属性时只是生成了set和get方法的声明,需要通过synthesize关键字生成的对应的成员变量以及setter和getter,随着编译器的发展,@property声明一个变量,编辑器就帮我们自动生成了对应的成员变量,setter和getter。

@synthesize 属性名 = 成员变量名;

dynamic
告诉编译器不要帮我生成setter和getter方法。如果一个属性被声明成@dynamic但有没有手动实现setter或者getter方法,在运行时就会crash。编译时不会,涉及到动态绑定。

atomic
原子性操作。是线程安全的。会对setter和getter方法进行加锁和解锁的操作。可以看到runtime对于属性赋值的底层实现reallySetProperty(),会判断属性是否是atomic,如果是atomic会有一个spinlock_t(自旋锁:),会有加锁和解锁的操作。atomic一定是安全的吗?👇

static inline void reallySetProperty(id self, SEL _cmd, id newValue, ptrdiff_t offset, bool atomic, bool copy, bool mutableCopy)
{
   // 此处删除了一些代码...为了看起来更方便
    if (!atomic) {
        oldValue = *slot;
        *slot = newValue;
    } else {
        spinlock_t& slotlock = PropertyLocks[slot];
        slotlock.lock();
        oldValue = *slot;
        *slot = newValue;        
        slotlock.unlock();
    }
    objc_release(oldValue);
}

nonatomic
属性的setter和getter方法里直接赋值。是线程不安全的。但平时的开发中,我们都会对属性用nonatomic关键字修饰。首先平时的开发中对属性的赋值是频繁操作的,频繁的加锁解锁会非常的耗性能。其次,多条线程访问同一属性的概率较低,如果真有这种情况存在的话,那么特殊情况特殊处理即可。

synchronize
把这个单独拎出来是因为和这个关键字synthesize长的有点像,也是为了提醒自己看到这个关键字的时候能想起多线程中加锁的方案。self是锁对象,不一定要传self,只要保证在加锁和解锁的时候是同一个对象即可。synchronized是对pthread_mutex递归锁的一个封装,从代码上来看,是最简单的。

    @synchronized (self) {
        // 需要同步的代码
    }

auto 与 static
auto关键字是系统默认的,定义一个局部变量都会有默认的auto关键字,因为是默认的,所以可以不写。auto也叫自动变量,离开对应的作用域就会自动被销毁。

static不管是修饰局部变量还是全局变量,都是放在数据区的,所以不会轻易被销毁。block内部对局部static变量会进行捕获,而且是指针捕获。这也是为什么,block内部可以修改static变量而不能修改auto变量的原因了。static也可以用来修饰全局变量,作用域是文件的定义到结束。外部可以通过extern来使用,所以如果不想外部使用的话,可以用关键字const来修饰。static修饰全局变量,block内部是不会捕获的,因为在全局数据区,可以直接通过地址访问,不需要新增成员进行捕获。

__block
主要有两个功能。1.__block修饰的auto变量可以在block内部进行修改。2.__block可以解决block和对象循环引用的问题。这在MRC上是因为__block包装的对象不会被block内部强引用。而在ARC上需要开发人员手动处理,即必须要调用block以及在block内部将对象置为nil。所以在ARC更推荐使用__weak来解决循环引用的问题。

5.问题

5.1 为什么字符串要用copy修饰?为什么block要用copy修饰?

copy保证字符串属性将来肯定是不可变的,如果外面想要修改字符串,直接给属性赋个新的值就好了,而不是拿到里面的字符串对象进行操作。字符串比较特殊,牵扯到UI显示的问题,UI控件显示的内容和外部应该互不影响。block为什么要用copy修饰见👉block本质

5.2 weak指针原理,当对象销毁时是如何被置为nil的?

见另一篇文章👉weak指针原理

5.3 OC中属性如果不写任何修饰词,默认是什么?atomic一定是安全的吗?

根据线程是否安全,读写权限,强弱引用分别是atomic readwrite strong

不一定。atomic只是对setter和getter方法进行了加锁和解锁的操作。仅仅只是保证了在setter和getter里面的操作是线程安全的。当把属性取出来操作的时候,仍然是不安全的。比如:

@property (nonatomic, strong) NSMutableArray *array;

当通过getter()取出array再进行addObject操作时仍然是不安全的。

总结

还有些问题,我还在学习。会不断更新这篇文章。最近的感悟,知识真的既像蜘蛛网一样,互相缠绕,又像无底洞一样,深不见底。