知识点3

26. 什么是KVC和KVO?

KVO:

iOS开发-KVO的奥秘

http://www.jianshu.com/p/742b4b248da9

iOS KVO(键值观察) 总览

http://www.jianshu.com/p/7e0ddc5f4e78

iOS--KVO的实现原理与具体应用

http://www.cnblogs.com/azuo/p/5442319.html

KVC:

iOS开发技巧系列---详解KVC(我告诉你KVC的一切)

http://www.jianshu.com/p/45cbd324ea65

【iOS】KVC 和 KVO 的使用场景

http://blog.csdn.net/chenglibin1988/article/details/38259865

KVC(Key-Value-Coding):键 - 值编码是一种通过字符串间接访问对象的方式。

而不是通过调用setter方法或通过实例变量访问的机制。很多情况下可以简化程序代码。

例如:

@interface MeiLing:NSObject

@property NSString *name;

@property UILabel *label;

@end

对于name的赋值 可以使用 meiLing.name = @"笑玲"; 这是点语法。调用的是setName方法。

KVC的写法是  [meiLing setValue:@"梦玲" forKey:@"name"];  通过name字符串赋值。

当然也可以跨层赋值,例如为label的text属性赋值

点语法: meiLing.label.text = @"笑玲";

KVC: [meiLing setValue:@"梦玲" forKeyPath:@"label.text"];

KVO:键值观察机制,他提供了观察某一属性变化的方法,极大的简化了代码。

KVO 只能被 KVC触发, 包括使用setValue:forKey:方法 和 点语法。

通过下方方法为属性添加KVO观察

- (void)addObserver:(NSObject *)observer

forKeyPath:(NSString *)keyPath

options:(NSKeyValueObservingOptions)options

context:(nullable void *)context;

当被观察的属性发生变化时,会自动触发下方方法

- (void)observeValueForKeyPath:(NSString *)keyPath

ofObject:(id)object

change:(NSDictionary *)change

context:(void *)context

41. 不手动指定autoreleasepool的前提下,一个autorealese对象在什么时刻释放?(比如在一个vc的viewDidLoad中创建)

分两种情况:手动干预释放时机、系统自动去释放。

手动干预释放时机--指定autoreleasepool 就是所谓的:当前作用域大括号结束时释放。

系统自动去释放--不手动指定autoreleasepool

Autorelease对象会在当前的 runloop 迭代结束时释放。

如果在一个vc的viewDidLoad中创建一个 Autorelease对象,

那么该对象会在 viewDidAppear 方法执行前就被销毁了。

不手动指定autoreleasepool的前提下,一个autorealese对象在什么时刻释放?(比如在一个vc的viewDidLoad中创建)

分两种情况:手动干预释放时机、系统自动去释放。

1.手动干预释放时机--指定autoreleasepool 就是所谓的:当前作用域大括号结束时释放。

系统自动去释放--不手动指定autoreleasepool

2.Autorelease对象出了作用域之后,会被添加到最近一次创建的自动释放池中,并会在当前的 runloop 迭代结束时释放。

释放的时机总结起来,可以用下图来表示:


知识点3_第1张图片


下面对这张图进行详细的解释:

从程序启动到加载完成是一个完整的运行循环,然后会停下来,等待用户交互,用户的每一次交互都会启动一次运行循环,来处理用户所有的点击事件、触摸事件。

我们都是知道:所有 autorelease 的对象,在出了作用域之后,会被自动添加到最近创建的自动释放池中。

但是如果每次都放进应用程序的 main.m

中的 autoreleasepool 中,迟早有被撑满的一刻。这个过程中必定有一个释放的动作。何时?

在一次完整的运行循环结束之前,会被销毁。

那什么时间会创建自动释放池?运行循环检测到事件并启动后,就会创建自动释放池。

子线程的 runloop 默认是不工作,无法主动创建,必须手动创建。

自定义的 NSOperation 和 NSThread 需要手动创建自动释放池。比如: 自定义的 NSOperation 类中的 main 方法里就必须添加自动释放池。否则出了作用域后,自动释放对象会因为没有自动释放池去处理它,而造成内存泄露。

但对于 blockOperation 和 invocationOperation 这种默认的Operation ,系统已经帮我们封装好了,不需要手动创建自动释放池。

@autoreleasepool 当自动释放池被销毁或者耗尽时,会向自动释放池中的所有对象发送 release 消息,释放自动释放池中的所有对象。

如果在一个vc的viewDidLoad中创建一个 Autorelease对象,那么该对象会在 viewDidAppear 方法执行前就被销毁了。

参考链接:《黑幕背后的Autorelease》

这个问题拿来做面试题,问过很多人,没有几个能答对的。很多答案都是“当前作用域大括号结束时释放”,显然木有正确理解Autorelease机制。在没有手加Autorelease Pool的情况下,Autorelease对象是在当前的runloop

迭代结束时释放的,而它能够释放的原因是系统在每个runloop迭代中都加入了自动释放池Push和Pop

__weak idreference =nil;

- (void)viewDidLoad { 

[superviewDidLoad];

NSString*str = [NSStringstringWithFormat:@"sunnyxx"];

// str是一个autorelease对象,设置一个weak的引用来观察它

reference = str;

}

- (void)viewWillAppear:(BOOL)animated {

 [superviewWillAppear:animated];

NSLog(@"%@", reference);

// Console: sunnyxx

}

- (void)viewDidAppear:(BOOL)animated {

 [superviewDidAppear:animated];

NSLog(@"%@", reference);

// Console: (null)

}

由于这个vc在loadView之后便add到了window层级上,所以viewDidLoad和viewWillAppear是在同一个runloop调用的,因此在viewWillAppear中,这个autorelease的变量依然有值。

当然,我们也可以手动干预Autorelease对象的释放时机:

- (void)viewDidLoad{

 [superviewDidLoad];

@autoreleasepool{

NSString*str = [NSStringstringWithFormat:@"sunnyxx"]; 

}

NSLog(@"%@", str);// Console: (null)}


我是前言

Autorelease机制是iOS开发者管理对象内存的好伙伴,MRC中,调用[obj autorelease]来延迟内存的释放是一件简单自然的事,ARC下,我们甚至可以完全不知道Autorelease就能管理好内存。而在这背后,objc和编译器都帮我们做了哪些事呢,它们是如何协作来正确管理内存的呢?刨根问底,一起来探究下黑幕背后的Autorelease机制。

Autorelease对象什么时候释放?

这个问题拿来做面试题,问过很多人,没有几个能答对的。很多答案都是“当前作用域大括号结束时释放”,显然木有正确理解Autorelease机制。

在没有手加Autorelease Pool的情况下,Autorelease对象是在当前的runloop迭代结束时释放的,而它能够释放的原因是系统在每个runloop迭代中都加入了自动释放池Push和Pop

小实验

__weak id reference = nil;

- (void)viewDidLoad {

[super viewDidLoad];

NSString *str = [NSString stringWithFormat:@"sunnyxx"];

// str是一个autorelease对象,设置一个weak的引用来观察它

reference = str;

}

- (void)viewWillAppear:(BOOL)animated {

[super viewWillAppear:animated];

NSLog(@"%@", reference); // Console: sunnyxx

}

- (void)viewDidAppear:(BOOL)animated {

[super viewDidAppear:animated];

NSLog(@"%@", reference); // Console: (null)

}

由于这个vc在loadView之后便add到了window层级上,所以viewDidLoad和viewWillAppear是在同一个runloop调用的,因此在viewWillAppear中,这个autorelease的变量依然有值。

当然,我们也可以手动干预Autorelease对象的释放时机:

- (void)viewDidLoad {

[super viewDidLoad];

@autoreleasepool {

NSString *str = [NSString stringWithFormat:@"sunnyxx"];

}

NSLog(@"%@", str); // Console: (null)

}

Autorelease原理

AutoreleasePoolPage

ARC下,我们使用@autoreleasepool{}来使用一个AutoreleasePool,随后编译器将其改写成下面的样子:

void *context = objc_autoreleasePoolPush();

// {}中的代码

objc_autoreleasePoolPop(context);

而这两个函数都是对AutoreleasePoolPage的简单封装,所以自动释放机制的核心就在于这个类。

AutoreleasePoolPage是一个C++实现的类


知识点3_第2张图片

AutoreleasePool并没有单独的结构,而是由若干个AutoreleasePoolPage以双向链表的形式组合而成(分别对应结构中的parent指针和child指针)

AutoreleasePool是按线程一一对应的(结构中的thread指针指向当前线程)

AutoreleasePoolPage每个对象会开辟4096字节内存(也就是虚拟内存一页的大小),除了上面的实例变量所占空间,剩下的空间全部用来储存autorelease对象的地址

上面的id *next指针作为游标指向栈顶最新add进来的autorelease对象的下一个位置

一个AutoreleasePoolPage的空间被占满时,会新建一个AutoreleasePoolPage对象,连接链表,后来的autorelease对象在新的page加入

所以,若当前线程中只有一个AutoreleasePoolPage对象,并记录了很多autorelease对象地址时内存如下图:


知识点3_第3张图片

图中的情况,这一页再加入一个autorelease对象就要满了(也就是next指针马上指向栈顶),这时就要执行上面说的操作,建立下一页page对象,与这一页链表连接完成后,新page的next指针被初始化在栈底(begin的位置),然后继续向栈顶添加新对象。

所以,向一个对象发送- autorelease消息,就是将这个对象加入到当前AutoreleasePoolPage的栈顶next指针指向的位置

释放时刻

每当进行一次objc_autoreleasePoolPush调用时,runtime向当前的AutoreleasePoolPage中add进一个哨兵对象,值为0(也就是个nil),那么这一个page就变成了下面的样子:


知识点3_第4张图片

objc_autoreleasePoolPush的返回值正是这个哨兵对象的地址,被objc_autoreleasePoolPop(哨兵对象)作为入参,于是:

根据传入的哨兵对象地址找到哨兵对象所处的page

在当前page中,将晚于哨兵对象插入的所有autorelease对象都发送一次- release消息,并向回移动next指针到正确位置

补充2:从最新加入的对象一直向前清理,可以向前跨越若干个page,直到哨兵所在的page

刚才的objc_autoreleasePoolPop执行后,最终变成了下面的样子:


知识点3_第5张图片

嵌套的AutoreleasePool

知道了上面的原理,嵌套的AutoreleasePool就非常简单了,pop的时候总会释放到上次push的位置为止,多层的pool就是多个哨兵对象而已,就像剥洋葱一样,每次一层,互不影响。

【附加内容】

Autorelease返回值的快速释放机制

值得一提的是,ARC下,runtime有一套对autorelease返回值的优化策略。

比如一个工厂方法:

+ (instancetype)createSark {

return [self new];

}

// caller

Sark *sark = [Sark createSark];

秉着谁创建谁释放的原则,返回值需要是一个autorelease对象才能配合调用方正确管理内存,于是乎编译器改写成了形如下面的代码:

+ (instancetype)createSark {

id tmp = [self new];

return objc_autoreleaseReturnValue(tmp); // 代替我们调用autorelease

}

// caller

id tmp = objc_retainAutoreleasedReturnValue([Sark createSark]) // 代替我们调用retain

Sark *sark = tmp;

objc_storeStrong(&sark, nil); // 相当于代替我们调用了release

一切看上去都很好,不过既然编译器知道了这么多信息,干嘛还要劳烦autorelease这个开销不小的机制呢?于是乎,runtime使用了一些黑魔法将这个问题解决了。

黑魔法之Thread Local Storage

Thread Local Storage(TLS)线程局部存储,目的很简单,将一块内存作为某个线程专有的存储,以key-value的形式进行读写,比如在非arm架构下,使用pthread提供的方法实现:

void* pthread_getspecific(pthread_key_t);

int pthread_setspecific(pthread_key_t , const void *);

说它是黑魔法可能被懂pthread的笑话- -

在返回值身上调用objc_autoreleaseReturnValue方法时,runtime将这个返回值object储存在TLS中,然后直接返回这个object(不调用autorelease);同时,在外部接收这个返回值的objc_retainAutoreleasedReturnValue里,发现TLS中正好存了这个对象,那么直接返回这个object(不调用retain)。

于是乎,调用方和被调方利用TLS做中转,很有默契的免去了对返回值的内存管理。

于是问题又来了,假如被调方和主调方只有一边是ARC环境编译的该咋办?(比如我们在ARC环境下用了非ARC编译的第三方库,或者反之)

只能动用更高级的黑魔法。

黑魔法之__builtin_return_address

这个内建函数原型是char *__builtin_return_address(int level),作用是得到函数的返回地址,参数表示层数,如__builtin_return_address(0)表示当前函数体返回地址,传1是调用这个函数的外层函数的返回值地址,以此类推。

- (int)foo {

NSLog(@"%p", __builtin_return_address(0)); // 根据这个地址能找到下面ret的地址

return 1;

}

// caller

int ret = [sark foo];

看上去也没啥厉害的,不过要知道,函数的返回值地址,也就对应着调用者结束这次调用的地址(或者相差某个固定的偏移量,根据编译器决定)

也就是说,被调用的函数也有翻身做地主的机会了,可以反过来对主调方干点坏事。

回到上面的问题,如果一个函数返回前知道调用方是ARC还是非ARC,就有机会对于不同情况做不同的处理

黑魔法之反查汇编指令

通过上面的__builtin_return_address加某些偏移量,被调方可以定位到主调方在返回值后面的汇编指令:

// caller

int ret = [sark foo];

// 内存中接下来的汇编指令(x86,我不懂汇编,瞎写的)

movq ??? ???

callq ???

而这些汇编指令在内存中的值是固定的,比如movq对应着0x48。

于是乎,就有了下面的这个函数,入参是调用方__builtin_return_address传入值

static bool callerAcceptsFastAutorelease(const void * const ra0) {

const uint8_t *ra1 = (const uint8_t *)ra0;

const uint16_t *ra2;

const uint32_t *ra4 = (const uint32_t *)ra1;

const void **sym;

// 48 89 c7    movq  %rax,%rdi

// e8          callq symbol

if (*ra4 != 0xe8c78948) {

return false;

}

ra1 += (long)*(const int32_t *)(ra1 + 4) + 8l;

ra2 = (const uint16_t *)ra1;

// ff 25      jmpq *symbol@DYLDMAGIC(%rip)

if (*ra2 != 0x25ff) {

return false;

}

ra1 += 6l + (long)*(const int32_t *)(ra1 + 2);

sym = (const void **)ra1;

if (*sym != objc_retainAutoreleasedReturnValue)

{

return false;

}

return true;

}

它检验了主调方在返回值之后是否紧接着调用了objc_retainAutoreleasedReturnValue,如果是,就知道了外部是ARC环境,反之就走没被优化的老逻辑。

其他Autorelease相关知识点

使用容器的block版本的枚举器时,内部会自动添加一个AutoreleasePool:

[array enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOL *stop) {

// 这里被一个局部@autoreleasepool包围着

}];

当然,在普通for循环和for in循环中没有,所以,还是新版的block版本枚举器更加方便。for循环中遍历产生大量autorelease变量时,就需要手加局部AutoreleasePool咯。

黑幕背后的Autorelease

http://blog.sunnyxx.com/2014/10/15/behind-autorelease/

42. 使用block时什么情况会发生引用循环,如何解决?

一个对象中强引用了block,在block中又使用了该对象,就会发射循环引用。

解决方法是将该对象使用__weak或者__block修饰符修饰之后再在block中使用。

id weak weakSelf = self;

或者

__weak __typeof(&*self)weakSelf = self该方法可以设置为宏,便于使用

例如

#define WS(weakSelf)  __weak __typeof(&*self)weakSelf = self;

WS(weakSelf)

[self.tableView addHeaderWithCallback:^{

[weakself requestMemberList];

}];

如果要在block内部改变外部变量的值,则需要如下定义

id __block weakSelf = self;

43. 以下代码运行结果如何?

-(void)viewDidLoad

{

[super viewDidLoad];

NSLog(@"1");

dispatch_sync(dispatch_get_main_queue(), ^{

NSLog(@"2");

});

NSLog(@"3");

}

答:

发生主线程锁死。程序出现假死状态.

//死锁原因

//1:dispatch_sync在等待block语句执行完成,而block语句需要在主线程里执行,所以dispatch_sync如果在主线程调用就会造成死锁

//2:dispatch_sync是同步的,本身就会阻塞当前线程,也即主线程。而又往主线程里塞进去一个block,所以就会发生死锁。

//});

//dispatch_async(dispatch_get_global_queue(), ^{

//async 在主线程中 创建了一个异步线程 加入 全局并发队列,async 不会等待block 执行完成,立即返回

NSLog(@2);//不会造成死锁;

});

}

分析这段代码:view DidLoad 在主线程中,也即dispatch_get_main_queue()中,执行到sync时向dispatch_get_main_queue()插入同步thread,sync会等到后面的block执行完成才返回。sync又在主队列里面,是个串行队列,sync是后面才加入的,前面一个是主线程,所以sync想执行block必须等待前一个主线程执行完成,而主线程却在等待sync返回,去执行后续工作,从而造成死锁。

2:

dispatch_sync 和 dispatch_async 区别:

dispatch_async(queue,block) async 异步队列,dispatch_async 函数会立即返回, block会在后台异步执行。

dispatch_sync(queue,block) sync 同步队列,dispatch_sync 函数不会立即返回,即阻塞当前线程,等待 block同步执行完成。

44. 若一个类有实例变量 NSString *_foo ,调用setValue:forKey:时,可以以foo还是 _foo 作为key?

都可以。这个考察了KVC机制。

45. 什么情况使用 weak 关键字,相比 assign 有什么不同?

什么情况使用 weak 关键字?

1)在ARC中,在有可能出现循环引用的时候,往往要通过让其中一端使用weak来解决,

比如:delegate代理属性

2)自身已经对它进行一次强引用,没有必要再强引用一次,此时也会使用weak,

自定义IBOutlet控件属性一般也使用weak;当然,也可以使用strong。

不同点:

1)weak 此特质表明该属性定义了一种“非拥有关系” (nonowning relationship)。

为这种属性设置新值时,设置方法既不保留新值,也不释放旧值。此特质同assign类似,

然而在属性所指的对象遭到摧毁时,属性值也会清空(nil out)。

而 assign 的“设置方法”只会执行针对“纯量类型”

(scalar type,例如 CGFloat 或 NSlnteger 等)的简单赋值操作。

2)assign 可以用非OC对象,而weak必须用于OC对象

46.怎么用 copy 关键字?

1) 用@property声明 NSString、NSArray、NSDictionary 经常使用copy关键字,

是因为他们有对应的可变类型:NSMutableString、NSMutableArray、NSMutableDictionary,

他们之间可能进行赋值操作,为确保对象中的字符串值不会无意间变动,应该在设置新属性值时拷贝一份。

不过通常为了节省系统资源,选择使用strong代替copy

2)block也经常使用copy关键字, 不过使用strong也无伤大雅.

47. @protocol 和 category 中如何使用 @property

1)在protocol中使用property只会生成setter和getter方法声明,我们使用属性的目的,

是希望遵守我协议的对象能实现该属性

2)category 使用 @property 也是只会生成setter和getter方法的声明,

如果我们真的需要给category增加属性的实现,需要借助于运行时的两个runtime函数:

①objc_setAssociatedObject

②objc_getAssociatedObject

48. @synthesize和@dynamic分别有什么作用?

1)@property有两个对应的词,一个是@synthesize,一个是@dynamic。

如果@synthesize和@dynamic都没写,那么默认的就是@synthesize var = _var;

2)@synthesize的语义是如果你没有手动实现setter方法和getter方法,

那么编译器会自动为你加上这两个方法。

3)@dynamic告诉编译器:属性的setter与getter方法由用户自己实现,不自动生成。(

当然对于readonly的属性只需提供getter即可)。假如一个属性被声明为@dynamic var,

然后你没有提供@setter方法和@getter方法,编译的时候没问题,

但是当程序运行到instance.var = someVar,由于缺setter方法会导致程序崩溃;

或者当运行到 someVar = var时,由于缺getter方法同样会导致崩溃。

编译时没问题,运行时才执行相应的方法,这就是所谓的动态绑定。

49. ARC下,不显式指定任何属性关键字时,默认的关键字都有哪些?

对应基本数据类型默认关键字是

atomic,readwrite,assign

对于普通的OC对象

atomic,readwrite,strong

MRC:手动内存释放。遵循谁申请谁释放的原则,需要手动的处理内存计数的增加和修改。从12年开始,逐步被ARC(自动内存释放)模式取代。 ↩

点语法: “self.属性 = obj” 调用属性的setter方法。”self.属性” 调用属性的getter方法区别在于是否有等号 。

你可能感兴趣的:(知识点3)