BAT面试的准备—iOS篇

本文中的全部面试题都来自面试BAT的学长或者同窗给个人一手记录,我将这些题目罗列并给予解答,一方面当作笔记,另外一方面造福你们

iOS网络层设计git

一、网络层和业务层的对接设计github

  • 使用哪一种交互模式来和业务层对接 : 使用Delegate为主,目的是为了(1)减小代码的分散度(2)减小业务层和网络层的耦合,网络层对于业务层应该是抽象的,隐藏了实现细节的 (3)只采用一种是限制了灵活性,方便进行维护面试

  • 在网络层不要滥用block :(1)block会延长对象的生命期,delegate则不会
    (2)block适合于在每次回调的任务都不同的状况下,若是同样则应使用delegate,苹果内部的网络层封装为delegate(离散型),AF的网络层封装为block(集约型)算法

  • 使用一个reformer对象来封装数据转化逻辑,从而节省了业务层进行字典转模型这样相似的繁琐操做,同时为了解决直接使用字典的可读性差的问题,采用KPropertyStudentID这样的const变量来做为字典的key。数据库

  • 使用离散型(delegate)的方式作网络层封装须要使用到继承,使用一个BaseAPIManager做为父类,来处理全部须要集约化的操做(例如一些公用信息),而后让不少子类来作离散化的操做编程

二、网络层的安全防范windows

  • 防止竞争对手使用本身的API,为本身的API设计一个签名,服务端给出一个密钥,在每次使用API的时候进行一个hash算法的操做,将hash出来的值和服务端hash出来的值进行一个对比,若是同样,则代表是本身在使用API缓存

  • 防止中间人攻击,使用较为安全的HTTPS协议,防止运营商在请求中加入广告安全

MVC模式和MVVM模式的区别微信

一、MVC模式存在Controller中代码臃肿的问题
之因此会出现MVC模式,是由于发如今开发中会有不少代码能够进行复用,同时事实也正是如此,MVC三个没款中,Model和View的代码确实能够由于MVC模式而进行复用,在github上也有不少开源的项目中封装了不少View,咱们能够很方便得使用这些view,model类做为一个数据转化逻辑的类也能够在同一个项目中进行屡次复用,可是Controller却很难在一个项目中进行复用,因此咱们在写代码的时候尽可能在Controller中只写那些没法复用的代码,例如将view添加到controller上,将model的数据传给view等等,可是实际上很难作到这一点,每每有不少代码咱们都不知道放在哪里,到了最后便放在了controller里面,致使controller变得十分臃肿。

二、对MVC模式中的Controller进行瘦身
咱们能够从下面几点对Controller进行瘦身:

  • 将添加view到controller上的代码进行抽取到自定义的UIView中去封装
  • 将网络请求抽取出来进行封装
  • 将数据获取和数据的转化逻辑抽取出来进行封装
  • 构造MVVM模式中的viewModel,其中封装了数据的转化逻辑

三、MVVM模式的认知

  • MVVM模式存在双向绑定技术,也就是说viewModel变化,那么model也跟着变化
  • 双向绑定会致使难以发现bug出现的位置
  • 在大型项目中双向绑定会致使内存消耗过大

四、总结
应该结合MVC和MVVM的各自的优势去让Controller进行瘦身,而不该该盲目地去追求新技术,亦或是过于保守,不肯意向前发展。

iOS中如何设置圆角

一、常规的设置方式带来的性能损耗
使用cornerRadius属性设置圆角是不会产生离屏渲染的,同时也不会真正在UI上产生圆角,这时候咱们须要将masksToBounds设置为YES,才可以产生在UI上的圆角效果,可是同时,这样也会致使离屏渲染。产生离屏渲染对于性能上有很大的消耗,将会下降FPS帧数,缘由是由于离屏渲染须要将图像的处理放在屏幕以外的内存缓存区进行处理,处理结束以后才把获得的图像放到主屏幕上。在这个过程当中产生最大消耗的是两次上下文的交互,将处理放到屏幕以外的缓存区,而后把获得的图像放到主屏幕上。

二、使用不产生离屏渲染的方式来创造圆角
使用贝塞尔曲线UIBezierPath和Core Graphics框架画出一个圆角

UIImageView *imageView = [[UIImageView alloc]initWithFrame:CGRectMake(100, 100, 100, 100)]; imageView.image = [UIImage imageNamed:@"1"]; //开始对imageView进行画图 UIGraphicsBeginImageContextWithOptions(imageView.bounds.size, NO, [UIScreen mainScreen].scale); //使用贝塞尔曲线画出一个圆形图 [[UIBezierPath bezierPathWithRoundedRect:imageView.bounds cornerRadius:imageView.frame.size.width] addClip]; [imageView drawRect:imageView.bounds]; imageView.image = UIGraphicsGetImageFromCurrentImageContext(); //结束画图 UIGraphicsEndImageContext(); [self.view addSubview:imageView];

三、总结

  • 若是屏幕中没有不少的圆角的话,那么就采用常规的方式设置便可
  • 若是屏幕中存在了大量的圆角的话,那么须要对圆角进行优化,防止离屏渲染

微信中点击头像放大动画的思路

建立一个背景和新的UIImageView,UIImageView是位于背景之上的,先把背景的透明度改成0,而后进行动画,动画的效果是将新的UIImageView从原始的位置(这个位置是原来的UIImageView在新的背景上对应的frame)变化到放大的位置,而后监听背景的点击事件,点击的时候进行透明度和frame的相反变化便可。具体过程我封装好了上传到Github了,点击这里查看。

线上项目出现bug怎么解决

这里将会涉及到JSPatch这个框架的使用,这个框架的做用就是对bug进行热修复..后续更新

iOS开发中有哪些状况会产生循环引用

一、block
二、delegate
三、NSTimer
解决办法:使用一个NSTimer的Catagory,而后重写初始化方法,在实现中利用block,从而在调用的时候可使用weakSelf在block执行任务

autoreleasePool(加入到autoreleasePool中的对象)在何时释放?


Runloop和AutoreleasePool
  • RunLoop启动的时候建立autoreleasePool
  • RunLoop结束的时候销毁autoreleasePool
  • 当RunLoop进行休眠的时候,将会将以前的autoreasePool销毁,同时建立新的autoreleasePool

iOS中的深浅复制

请查看这篇文章,讲得很深刻:iOS剖析深浅复制

iOS中的属性修饰符

列举的顺序就是修饰符在声明的时候的顺序

一、原子性修饰符

  • nonatomic:通常对于属性都采用nonatomic来修饰,若是须要保证线程安全,则手动添加代码进行保护
  • atomic:默认是atomic,使用该修饰符,系统会对属性进行原子性的保护操做,保证线程安全,可是会有性能损耗

二、读写权限修饰符

  • readonly:只读
  • readwrite:默认为readwrite修饰

三、内存管理修饰符

  • strong:强引用,主要有任何strong类型的指针指向对象,那么就不会被ARC销毁
  • copy:主要用于NSString、NSArray、NSDictionary以及block,前者是由于他们都有可变类型,后者是由于在MRC中,使用copy可以将block从栈区拷贝到堆区,在ARC中使用strong和copy效果同样,可是写上copy仿佛在时刻提醒着咱们编译器帮咱们进行了copy操做
  • weak:弱引用:表示定义了一种非拥有关系,若是属性所指向的对象被销毁了,那么属性值也会被清空,设置为nil指针
  • assign:对于基本数据类型的修饰,只会单纯进行赋值操做
  • unsafe_unretained:由unsafe和unretained组成,unretained和weak类似,unsafe表示他是不安全的,可能引发野指针的出现,致使crash
  • retain:ARC中引入了strong和weak,retain效果和strong等同

四、读写方法名修饰符

  • setter:修改setter方法的名字
  • getter:修改getter方法的名字

iOS中属性内存管理修饰符中的那些CP

  • strong vs copy

    self.name = anotherName;

    例如上面的代码,使用strong表示的是self.name和anotherName这两个指针同时指向了一个对象,过程是self.name指向了anotherName指向的对象,而若是使用copy的话,self.name和anotherName这两个指针同时指向了不一样的对象,过程是copy会将anotherName所指向的对象拷贝一份出来(浅拷贝),而后让self.name指向这个被拷贝出来的对象。

  • strong vs weak
    只要存在strong类型修饰的属性(指针)指向了一个对象,那么这个对象就不会被ARC销毁,可是对于weak类型修饰的属性(指针)指向了一个对象,若是这个对象被销毁了,那么这个属性(指针)就会被自动设置为nil。能够说weak类型的指针是没有约束做用的,只是简单弱弱地表示了一下关系。
    这里还须要分析在声明控件到底应该使用strong仍是weak

    • 若是是使用storyboard:


      storyboard引用关系图
    • 若是是使用纯代码:


      纯代码引用关系图
    • 综上,都应该使用weak去声明控件,纯代码中若是使用了strong去声明控件,那么有一种状况:若是将控件remove了,那么controller中的view里面的subviews所引用的那条线将会被切断,可是strong属性(指针)所引用的这条线依然存在,因为采用的是强引用,因此控件将不会被ARC给销毁,那么就会一直占用内存,直到控制器销毁。

  • weak vs assign
    weak只能用于对象类型,assign能够用于基本类型,weak比起assign有一点更好,若是weak修饰的属性指向的一个对象被销毁了,那么这个属性将会自动被设置为nil指针,若是assign修饰的属性指向的一个对象被销毁了,那么这个属性不会被自动设置为nil,同时他也不知道所指向的对象已经被销毁了,这样就引起了野指针。

  • weak vs unsafe_unretained
    若是weak修饰的属性指向的一个对象被销毁了,那么这个属性将会自动被设置为nil指针,若是unsafe_unretained修饰的属性指向的一个对象被销毁了,那么这个属性不会被自动设置为nil,同时他也不知道所指向的对象已经被销毁了,这样就引起了野指针。

  • assign vs unsafe_unretained
    assign能修饰基本类型,unsafe_unretained只能修饰对象类型

iOS中的多线程

一、pthread
基于C语言,不经常使用
二、NSThread
须要本身手管理线程的生命周期,偶尔使用,例如获取当前线程

[NSThread currentThread];

三、GCD(Grand Central Dispatch)
GCD是苹果开发出来的多核编程的解决方案,虽然是基于C语言的,可是采用了block进行封装,使用起来也很方便,同时也很重要,推荐使用GCD进行多线程编程
四、NSOperation
是苹果对于GCD的封装,效率不及GCD

iOS中的GCD

  • 常规使用

主队列:是一个特殊的串行队列,在主线程中运行,用于刷新UI,是一个串行队列

//串行队列 dispatch_queue_t queue = dispatch_get_main_queue;

自定义建立队列: 既能够建立串行队列也能够建立并行队列。

//串行队列 dispatch_queue_t queue = dispatch_queue_create("nineteen", NULL); dispatch_queue_t queue = dispatch_queue_create("nineteen", DISPATCH_QUEUE_SERIAL); //并行队列 dispatch_queue_t queue = dispatch_queue_create("nineteen", DISPATCH_QUEUE_CONCURRENT);

全局并行队列:系统提供的并行队列

//并行队列 dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0); dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0); dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_LOW, 0); dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0);
  • 其余使用

循环执行任务:dispatch_apply相似一个for循环,并发地执行每一项。全部任务结束后,dispatch_apply才会返回,会阻塞当前线程(相似同步执行)。

dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0); /* *count: 循环执行次数 *queue: 队列,能够是串行队列或者是并行队列(使用串行队列可能致使死锁) *block: 任务 */ dispatch_apply(count, queue, ^(size_t i) { NSLog(@"%zu %@", i, [NSThread currentThread]); });

队列组:队列组将不少队列添加到一个组里,当组里全部任务都执行完后,它会经过一个方法通知咱们。基本流程是首先建立一个队列组,而后把任务添加到组中,最后等待队列组的执行结果。

//建立队列组 dispatch_group_t group = dispatch_group_create(); //建立队列 dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0); //并行队列执行3次循环 (队列组只能用异步方法执行) dispatch_group_async(group, queue, ^{ for (NSInteger i = 0; i < 3; i++) { NSLog(@"group-01 - %@", [NSThread currentThread]); } }); //主队列执行5次循环 dispatch_group_async(group, dispatch_get_main_queue(), ^{ for (NSInteger i = 0; i < 5; i++) { NSLog(@"group-02 - %@", [NSThread currentThread]); } }); //都完成后会自动通知 dispatch_group_notify(group, dispatch_get_main_queue(), ^{ NSLog(@"完成 - %@", [NSThread currentThread]); });

实现单例模式

static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ //dispatch_once中的代码只执行一次,经常使用来实现单例 });

GCD延迟操做

//建立队列 dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0); //设置延时,单位秒 double delay = 3; dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(delay * NSEC_PER_SEC)), queue, ^{ //3秒后须要执行的任务 });

GCD中的死锁场景

五个案例了解GCD的死锁
一、
案例:

NSLog(@"1"); // 任务1 dispatch_sync(dispatch_get_main_queue(), ^{ NSLog(@"2"); // 任务2 }); NSLog(@"3"); // 任务3

结果:

1

二、
案例:

NSLog(@"1"); // 任务1 dispatch_sync(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{ NSLog(@"2"); // 任务2 }); NSLog(@"3"); // 任务3

结果:

1 2 3

三、
案例:

dispatch_queue_t queue = dispatch_queue_create("com.demo.serialQueue", DISPATCH_QUEUE_SERIAL); NSLog(@"1"); // 任务1 dispatch_async(queue, ^{ NSLog(@"2"); // 任务2 dispatch_sync(queue, ^{ NSLog(@"3"); // 任务3 }); NSLog(@"4"); // 任务4 }); NSLog(@"5"); // 任务5

结果:

1 5 2 // 5和2的顺序不必定

四、
案例:

NSLog(@"1"); // 任务1 dispatch_async(dispatch_get_global_queue(0, 0), ^{ NSLog(@"2"); // 任务2 dispatch_sync(dispatch_get_main_queue(), ^{ NSLog(@"3"); // 任务3 }); NSLog(@"4"); // 任务4 }); NSLog(@"5"); // 任务5

结果:

1 2 5 3 4 // 5和2的顺序不必定

五、
案例:

dispatch_async(dispatch_get_global_queue(0, 0), ^{ NSLog(@"1"); // 任务1 dispatch_sync(dispatch_get_main_queue(), ^{ NSLog(@"2"); // 任务2 }); NSLog(@"3"); // 任务3 }); NSLog(@"4"); // 任务4 while (1) { } NSLog(@"5"); // 任务5

结果:

1 4 // 1和4的顺序不必定

iOS中的递归锁

若是加锁操做处于一个循环或者递归中,在第一次加锁尚未解锁的时候,就进行了第二次加锁,因此就形成死锁现象,这时候应该使用递归锁来防止死锁的发生。

iOS中的ARC是怎么解决内存管理问题的

ARC会自动处理对象的声明周期,编译的时候在合适的地方插入内存管理代码

ARC中autorelease的使用场景

  • 函数返回对象的时候:函数对象做为返回值过了做用域的时候应该被销毁,可是这时候可能尚未被赋值(被强引用),因此须要将该对象添加到自动释放池中延长生命周期。
  • _weak修饰属性的时候:_weak修饰的属性所指向的对象可能没有一个强引用来引用他,可能会被销毁,这时候就须要对其使用autorelease方法保证他不被销毁
  • id 的指针或对象的指针

iOS中的RunLoop

通常主线程会自动运行RunLoop,咱们通常状况下不会去管。在其余子线程中,若是须要咱们须要去管理。使用RunLoop后,能够把线程想象成进入了一个循环;若是没有这个循环,子线程完成任务后,这个线程就结束了。因此若是须要一个线程处理各类事件而不让它结束,就须要运行RunLoop。

SDWebImage是怎么使用RunLoop的

- (void)start{ ... self.connection = [[NSURLConnection alloc] initWithRequest:self.request delegate:self startImmediately:NO]; ... if(self.connection){ ... CFRunLoopRun( ) ... } }
- (void)cancelInternalAndStop { if (self.isFinished) return; [self cancelInternal]; CFRunLoopStop(CFRunLoopGetCurrent()); }

在建立self.connection成功后,执行了CFRunLoopRun(),开启了runloop。在failed或finished的时候会调用CFRunLoopStop中止runloop。若是不开启runloop的话,在执行完start ()后任务就完成了,NSURLConnection的代理就不会执行了。runloop至关于子线程的循环,能够灵活控制子线程的生命周期。

AFNetworking是怎么使用RunLoop的

AFNetworking解决这个问题采用了另外一种方法:单独起一个global thread,内置一个runloop,全部的connection都由这个runloop发起,回调也都由它接收。这是个不错的想法,既不占用主线程,又不耗CPU资源:

iOS中的响应链

  • 经过官方文档提供的图来看看事件响应
    两种方式,因为设置不一样,但大体过程是同样的

事件响应
  • 具体的响应过程
    1. 发生了触摸事件后,系统会将该事件加入到UIApplication管理的一个队列中
    2. UIApplication从队列中取出最前面的事件,而后将事件传递下去,处理的顺序大体为UIApplication->AppDelegate->UIWindow->UIViewController->superView->subViews
    3. 一般来讲UIApplication会将事件先交给keyWindow来处理,keyWindow会找到一个最合适的视图来处理这个事件,处理的第一步就从这里开始了
    4. keyWindow是这样来找到合适视图的:调用hitTest:withEvent方法去寻找可以处理触摸事件的视图,hitTest:withEvent方法会递归地检查view以及view的子类是否包含了触摸点,像这样一直递归下去,找到离用户最近同时包含了触摸点的一个view,而后将触摸事件传递给这个view。在这个过程当中是经过PointInside:withEvent:方法来判断是否包含了触摸点的,若是包含了,就返回YES,若是没有包含就返回NO,而后hitTest:withEvent这个方法就返回nil,将再也不对该视图的子视图进行判断。

NSRunloop、runloop、autoreleasePool、thread

  • NSRunloop:NSRunloop是一个消息循环,它会检测输入元和定时源,而后作回调处理。NSRunloop封装了windows中的消息处理,将SendMessage、PostMessage、GetMessage等细节封装了起来。关于NSRunloop须要着重了解这几点内容:

    1. NSRunloop用来监听耗时的异步事件,例如网络回调
    2. NSRunloop解决了CPU空转问题,当没有任何事件须要处理的时候,NSRunloop会把线程调整为休眠状态,从而消除CPU的周期轮询。
    3. 每个线程都有一个NSRunloop,主线程是默认运行的,其余线程默认是没有运行的,须要在NSRunloop中添加一个事件,而后去启动这个线程的runloop。
  • runloop:新建iOS项目的时候会看到在main方法中会手动建立一个autoreleasePool,程序开始时建立,结束时销毁,若是只是从表面上来看的话,那么这样和内存泄露是没有什么区别的。其实,对于每个runloop,系统会隐式地建立一个autoreleasePool,这样全部的autoreleasePool构成一个栈式的结构,在每个runloop结束的时候,当前栈顶的autoreleasePool就会被弹出,同时销毁,其中的全部对象也一样被销毁。这里所指的runloop不是NSRunloop,这里的runloop多是一个UI事件,一个timer等等,具体来讲指的是从接受到消息,处处理完这个消息的一个完整过程。

  • autoreleasePool和thread
    thread是不会自动建立autoreleasePool的

drawRect的做用

  • drawRect方法的目的是进行UIView的绘制,使用的时候将绘制的具体内容写在drawRect方法里面
  • 苹果不建议直接使用drawRect方法,而是调用setNeedsDisplay方法,系统接着会自动调用drawRect进行UIView的绘制

layoutSubviews的做用

  • layoutSubviews的做用是对子视图进行从新布局
  • 苹果不建议直接使用该方法,而是经过调用setLayoutSubviews,让系统去自动调用layoutSubviews方法进行布局
  • 下面列出在何时会出发layoutSubviews方法
    1. 直接调用setLayoutSubviews。(这个在上面苹果官方文档里有说明)
    2. addSubview的时候。
    3. 当view的frame发生改变的时候。
    4. 滑动UIScrollView的时候。
    5. 旋转Screen会触发父UIView上的layoutSubviews事件。
    6. 改变一个UIView大小的时候也会触发父UIView上的layoutSubviews事件。

自定义控件

  • 自定义控件分为两种,一种是经过xib,另外一种是经过纯代码的方式
  • 自定义控件须要在两个方法中进行重写,达到开发者想要的效果
    1. initWithFrame:通常来讲是重写这个方法而不是init方法,由于init方法最终也会调用到initWithFrame方法,这个方法主要是对控件以及控件的子控件进行一些初始化的设置(若是是经过xib的话则是重写awakFromNib方法)
    2. layoutSubviews:这个方法是描述子控件如何布局,也就是赋予子控件在自定义控件中的位置关系,因此说对于子控件的frame的设置代码不该该放在initWithFrame中,而是应该放在layoutSubviews这个方法里面

数据持久化的几种方式的对比

  • Plist文件(属性列表):
    plist文件是将某些特定的类,经过XML文件的方式保存在目录中,这些类包括(若是存在对应的可变类也包括可变类)
    NSArray
    NSDictionary
    NSData
    NSString
    NSNumber
    NSDate

  • Preference(偏好设置):

    1. 偏好设置是专门用来保存应用程序的配置信息的
    2. 若是没有调用synchronize方法,系统会根据I/O状况不定时刻地保存到文件中。因此若是须要当即写入文件的就必须调用synchronize方法。
    3. 偏好设置会将全部数据保存到同一个文件中。即preference目录下的一个以此应用包名来命名的plist文件。
  • NSKeyedArchiver(归档):
    归档在iOS中是另外一种形式的序列化,只要遵循了NSCoding协议的对象均可以经过它实现序列化

  • SQLite 3
    以前的全部存储方法,都是覆盖存储。若是想要增长一条数据就必须把整个文件读出来,而后修改数据后再把整个内容覆盖写入文件。因此它们都不适合存储大量的内容,而SQLite 3却能更好进行大量内容的读写操做。
  • CoreData
    苹果封装的本地数据库,通常用于规划应用中的对象

app的状态

  • Not running:app还没运行
  • Inactive:app运行在foreground但没有接收事件
  • Active:app运行在foreground和正在接收事件
  • Background:运行在background和正在执行代码
  • Suspended:运行在background但没有执行代码

UIView和CALayer的区别

  • UIView能够响应事件,而CALayer不行
  • 一个 Layer 的 frame 是由它的 anchorPoint,position,bounds,和 transform 共同决定的,而一个 View 的 frame 只是简单的返回 Layer的 frame,一样 View 的 center和 bounds 也是返回 Layer 的一些属性。
  • UIView主要是对显示内容的管理而 CALayer 主要侧重显示内容的绘制。
  • 在作 iOS 动画的时候,修改非 RootLayer的属性(譬如位置、背景色等)会默认产生隐式动画,而修改UIView则不会。

KVO的实现原理

  • KVO是什么:
    KVO提供一种机制,指定一个被观察对象(例如A类),当对象某个属性(例如A中的字符串name)发生更改时,对象会得到通知,并做出相应处理
  • KVO的原理:
    1. NSKVONotifying_A:
      在这个过程,被观察对象的 isa 指针从指向原来的A类,被KVO机制修改成指向系统新建立的子类 NSKVONotifying_A类,来实现当前类属性值改变的监听;因此当咱们从应用层面上看来,彻底没有意识到有新的类出现,这是系统“隐瞒”了对KVO的底层实现过程,让咱们误觉得仍是原来的类。
    2. 子类setter方法剖析:KVO的键值观察通知依赖于 NSObject 的两个方法:willChangeValueForKey:和 didChangevlueForKey:,在存取数值的先后分别调用2个方法:被观察属性发生改变以前,willChangeValueForKey:被调用,通知系统该 keyPath 的属性值即将变动;当改变发生后, didChangeValueForKey: 被调用,通知系统该 keyPath 的属性值已经变动;以后, observeValueForKey:ofObject:change:context: 也会被调用。



文/NtZheng(简书做者) 原文连接:http://www.jianshu.com/p/be6197d00de9 著做权归做者全部,转载请联系做者得到受权,并标注“简书做者”。
相关文章
相关标签/搜索