zoukankan      html  css  js  c++  java
  • iOS的一些面试题分析总结(0)

    虽然一些东西在实际工作中我们是很少用到的,但是面试确实会经常问到一些我们不常用的东西,所以说有时候看一看还是有必要的,一方面面试也是很重要的一件事,另一方面某些情况下也能帮我们查漏补缺。

    一、NSNotification和KVO的区别和用法是什么?什么时候应该使用通知,什么时候应该使用KVO,它们的实现上有什么区别吗?如果用protocol和delegate(或者delegate的Array)来实现类似的功能可能吗?如果可能,会有什么潜在的问题?如果不能,为什么?

    答:

    这个问题涉及到了通知、代理和KVO,那么就一个个来,先回顾一下代理是怎么回事。

     代理:

    简单说一下我所理解的代理吧:比如说有两个对象,对象A想做一件事,但是它自己不方便去做,而对象B很适合去做这件事,那么B就可以作为A的委托去做这件事,而A就在适当的时机通知B去做这件事,并将做这件事所需要的方式和信息通知给B。初学的时候经常用它来在控制器之间传值,但是我觉得这样有点大材小用了。传值还是不必要用代理这么重量级的东西。下面看一下关于代理的小demo,为了简单我用它传了一个值,但是我觉得在真正需要传值的场合,还是没必要用代理,这里只为了演示代理是怎么回事。

    声明代理的代码:

     1 #import <UIKit/UIKit.h>
     2 
     3 @protocol ATNextViewControllerDelegate <NSObject>
     4 
     5 @optional
     6 - (void)myBtnColor:(UIColor *)color;
     7 
     8 //@required
     9 
    10 @end
    11 
    12 @interface ATNextViewController : UIViewController
    13 @property (nonatomic, weak) id<ATNextViewControllerDelegate> delegate;
    14 @end

    通知代理做事:

    1 - (void)popClick:(UIButton *)btn {
    2     if ([self.delegate respondsToSelector:@selector(myBtnColor:)]) {
    3         [self.delegate myBtnColor:btn.titleLabel.textColor];
    4     }
    5     
    6     [self.navigationController popViewControllerAnimated:YES];
    7     
    8 }

    代理控制器真正做事的地方:

    1 #pragma mark - ATNextViewControllerDelegate
    2 - (void)myBtnColor:(UIColor *)color {
    3     [self.btn setTitleColor:color forState:UIControlStateNormal];
    4 }

    demo:https://github.com/alan12138/Interview-question/tree/master/1/%E4%BB%A3%E7%90%86

    需要注意到的是:delegate与protocol没有关系。Delegate本身应该称为一种设计模式,是把一个类自己需要做的一部分事情,让另一个类(也可以就是自己本身)来完成,而实际做事的类为delegate。而protocol是一种语法,它的主要目标是提供接口给遵守协议的类使用,而这种方式提供了一个很方便的、实现delegate模式的机会

    总结:1、代理可以用来监听值的改变(在值改变的时候通知代理),并把改变的值传递给代理(传值)2、也可以用来监听一些行为的发生(执行某个方法的时候通知代理)

    下面再看通知:

    通知:

    通知个人感觉比代理更加灵活,而且代理通常是一对一的关系:一个对象作为另一个对象的代理,监听它的行为或属性值。但是通知可以是一对多的关系,一个对象通过通知中心发送消息,其他对象只要监听了这个通知便能接收到这个通知,并做相应处理;同时,一个对象可以监听多个通知。这里我做了两个demo,一个是常用的键盘监听,就是用通知来做的;另一个是一对多通知的演示。我们先看键盘监听:因为键盘监听是系统自动发出通知,所以我们只需要注册监听者就可以了。

    监听者一般需要做三个步骤:

    1、监听通知

    1 // 监听键盘通知
    2     [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(keyboardWillChangeFrame:) name:UIKeyboardWillChangeFrameNotification object:nil];

    2、定义收到通知的行为

     1 #pragma mark - 键盘处理
     2 - (void)keyboardWillChangeFrame:(NSNotification *)note {
     3     // 取出键盘最终的frame
     4     CGRect rect = [note.userInfo[UIKeyboardFrameEndUserInfoKey] CGRectValue];
     5     // 取出键盘弹出需要花费的时间
     6     double duration = [note.userInfo[UIKeyboardAnimationDurationUserInfoKey] doubleValue];
     7     // 修改transform
     8     [UIView animateWithDuration:duration animations:^{
     9         CGFloat ty = [UIScreen mainScreen].bounds.size.height - rect.origin.y;
    10         self.view.transform = CGAffineTransformMakeTranslation(0, - ty);
    11     }];
    12 }

    3、对象销毁的时候移除通知

    1 - (void)dealloc
    2 {
    3     [[NSNotificationCenter defaultCenter] removeObserver:self];
    4 }

    有人可能会问如果不移除通知会有什么后果,答案就是直接复制过来了:
    Before an object that is observing notifications is deallocated, it must tell the notification center to stop sending it notifications. Otherwise, the next notification gets sent to a nonexistent object and the program crashes. You can send the following message to completely remove an object as an observer of local notifications, regardless of how many objects and notifications for which it registered itself.

    下面继续看自己发送通知和多个对象监听通知的情况:

    0、发送通知(在需要被监听的控制器发送通知)

    1 [[NSNotificationCenter defaultCenter] postNotificationName:@"changeMyColor" object:self userInfo:dict];

    其中self表示的是发送这个通知的对象,userInfo参数表示的是发送出去的消息,name就不需要解释了,是通知的名字。

    1、接收通知(为保证安全性可以加一层判断,检测是否实现了通知触发方法)

    1  if ([self respondsToSelector:@selector(changeMyColor:)]) {
    2         [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(changeMyColor:) name:@"changeMyColor" object:nextVc];
    3     }

    self表示为自己添加监听对象,object参数表示的是,监听哪个对象,如果是nil,表示监听所有对象的名为changeMyColor的通知。

    2、通知触发

    1 - (void)changeMyColor:(NSNotification *)noti {
    2     [self.btn setTitleColor:(noti.userInfo)[@"btnColor"] forState:UIControlStateNormal];
    3 }

    3、移除通知

    1 - (void)dealloc {
    2     [[NSNotificationCenter defaultCenter] removeObserver:self];
    3 }

    另一个监听对象也是这个步骤,自己看咯。

    总结一下:通知可以监听键盘等用代理监听比较困难的事件,通知也可以灵活的为其他对象发送消息,以及接收其他对象发出的消息。

    最后,提醒一下观察者收到通知的顺序是没有定义的。同时通知发出和观察的对象有可能是一样的。通知中心同步转发通知给观察者,就是说 postNotification: 方法直到接收并处理完通知才返回值。要想异步的发送通知,可以使用NSNotificationQueue。在多线程编程中,通知一般是在一个发出通知的那个线程中转发,但也可能是不在同一个线程中转发通知。

    demo:https://github.com/alan12138/Interview-question/tree/master/1

    KVO:

    这里先简单介绍一下KVO,然后看一下KVO是怎么用的,最后简单说一下KVO实现的原理。

    KVO简单来说就是我们可以为某个Model对象的某个数据(Key)注册一个监听器,一旦Model的这个Key的Value发生变化,就会广播给监听它的所有的监听器。它有点类似于Notification,但是,对于监听属性值来说,用KVO比通知是方便很多的,如果用通知的话,会比用KVO多写一些额外的代码,读者可以自己试一下。

    这种观察-被观察模型适用于这样的情况,比方说根据A(数据类)的某个属性值变化,B(view类)中的某个属性做出相应变化。对于推崇MVC的cocoa而言,KVO应用价值很高。

    下面看一下KVO的代码:

    1、注册监听

    1 //为model对象的name属性添加self监听
    2     //keyPath就是要观察的属性值,options给你观察键值变化的选择,而context方便传输你需要的数据(注意这是一个void型)
    3     [model addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew context:@"123"];

    2、触发监听

    1 //监听key发生改变的时候触发的方法
    2 //change里存储了一些变化的数据,比如变化前的数据,变化后的数据;如果注册时context不为空,这里context就能接收到。
    3 - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary<NSString *,id> *)change context:(void *)context {
    4     NSLog(@"next:%@  %@  %@",keyPath,change,context);
    5 }

    3、移除监听

    1 - (void)dealloc {
    2     //在控制器销毁的时候为self.model移除self监听
    3     [self.model removeObserver:self forKeyPath:@"name"];
    4 }

    demo:https://github.com/alan12138/Interview-question/tree/master/1/KVO%E7%9B%91%E5%90%AC

    最后简单说一下我所理解的KVO的实现原理:KVO的实现利用runtime动态生成了被监听类的子类,并在被监听类的被监听属性改变的时候调用了生成的子类的set方法,并通过这个set方法触发了observeValueForKeyPath: ofObject: change: context:从而实现了对属性的监听。

    有没有发现KVO的步骤和通知很像,那么我们就先回答一下第一个小问题,各自用法上边已经写的很清楚了,下面简单说一下KVO和通知的区别吧:

    * 通知是需要一个发送notification的对象,一般是notificationCenter,来通知观察者。KVO是直接通知到观察对象,并且逻辑非常清晰,实现步骤简单。

    * 通知需要自己定义一个通知的名字,也就是一个字符串常量,这就造成很容易出错,而且不容易查找。通知还可以自己定义一个字典消息,并把它传递给任何监听这个通知的一个    或多个对象,同时,作为一个监听者,也可以监听一个或多个对象的通知。

    * 而KVO则没有通知名字的概念,只需要属性名作为字符串的key。1个对象的属性能被多个对象监听,  1个对象能监听多个对象的其他属性。KVO还能选择监听属性改变前的值或      属性改变之后的值。KVO还能通过context传递任何类型的信息。KVO和KVC一样,都能直接得到对象的私有属性。

    * 通知和KVO都能实现一对多或多对一的监听。KVO性能不好(底层会动态产生新的类),只能监听某个对象属性的改变,本人较少使用。并且,对于本人来说,通知也只有在类似监    听键盘这种用代理比较难以实现的场合才用。

    最后说一下最后一个问题,我的答案是delegate的array是可能的,但是感觉违背了delegate的设计初衷,delegate不是用来这么用的,所以不该这么用。不说设计的严谨性和安全性,再说一下代理比通知和KVO更多的一点是,代理是可以返回值给被代理对象的。

    最后说一下控制器传值,用代理是有点繁琐的,个人感觉用block是最方便的,具体可以参考我的博客中有关控制器传值的文章。

    参考博客(本文有些话是直接从这里摘抄下来的,感觉其中说的东西比较多,我只把自己最浅显的一些理解写在了自己的博客中,读者如果觉得还不够,可以看一下这篇文章):

    http://blog.csdn.net/dqjyong/article/details/7685933

    到这里这个问题已经算基本结束了,但是即然说到了KVO,那就顺便也把KVC作为补充内容写一下:

    KVC:

    1、简单的赋值和取值(KVC能直接访问私有属性)

    1  Person *p = [[Person alloc] init];
    2  [p setValue:@"畅敏" forKey:@"name"];
    3  NSLog(@"%@",[p valueForKey:@"name"]);

    2、通过路径获取

    1 Dog *d = [[Dog alloc] init];
    2 Person *p = [[Person alloc] init];
    3 p.dog = d;
    4 [p.dog setValue:@"旺财" forKeyPath:@"dogName"];
    5 NSLog(@"%@",[p.dog valueForKeyPath:@"dogName"]);
    6 //两种方式
    7 [p setValue:@"旺财" forKeyPath:@"dog.dogName"];
    8 NSLog(@"%@",[p valueForKeyPath:@"dog.dogName"]);

    3、同时给多个属性赋值

     1 NSDictionary *dict = @{
     2                            @"name" : @"fangfang",
     3                            @"gender" : @"girl",
     4                            @"age" : @18,
     5                            @"hobby" : @"fangfang"
     6                            };
     7     Person *p = [[Person alloc] init];
     8     
     9     // 同时给多个属性赋值
    10     [p setValuesForKeysWithDictionary:dict];
    11     
    12     NSLog(@"name = %@, gender = %@, age = %@, hobby = %@", [p valueForKey:@"name"], [p valueForKey:@"gender"], [p valueForKey:@"age"],[p valueForKey:@"hobby"]);

    关于KVC报错,这个在字典转模型的时候可能会遇到,就是在使用KVC的时候在当前类没有找到相应的key,便会抛出异常,抛出异常的方法是setValue: forUndefinedKey:(设值)或者valueForUndefinedKey(取值),这时候只要重写一下这个方法不做处理就可以了。

    1 // 重写
    2  // 使用KVC设置值对象
    3 - (void)setValue:(id)value forUndefinedKey:(NSString *)key
    4 {
    5     NSLog(@"不存在Key:%@", key);
    6 }
    1 // 重写
    2 // 使用KVC取值的时候
    3 - (id)valueForUndefinedKey:(NSString *)key
    4 {
    5     return nil;
    6 }

    实现机制:

    * 检查是否存在对应key的getter或者setter方法;        

    * 如果没有上述方法,则检查是否存在名字为-_<key>、<key>的实例变量;        

    * 如果仍未找到,则调用 valueForUndefinedKey: 和 setValue: forUndefinedKey: 方法。这些方法的默认实现都是抛出异常,我们可以根据需要重写它们。

     demo:https://github.com/alan12138/Interview-question/tree/master/1/KVC%E7%9A%84%E7%AE%80%E5%8D%95%E4%BD%BF%E7%94%A8

    KVC在字典转模型的时候用的比较多,具体可以参考我的博客中对runtime的介绍的那篇文章,详细介绍了利用runtime字典转模型,其中就用到了KVC。

    2、block内部的实现原理

    1、block的简单定义和使用

    1 //block的定义(NSString *类型是参数)
    2     //返回值类型和return返回的类型要严格匹配,否则编译直接报错
    3     //类似于这种“Incompatible block pointer types initializing 'float (^__strong)(float, float)' with an expression of type 'int (^)(float, float)'”
    4     CGFloat (^myBlock)(NSString *) = ^(NSString *name) {
    5         return 2.0;
    6     };
    7     
    8     NSLog(@"%lf",myBlock(@"q"));

    关于使用block传值可以看一下我的博客中关于控制器之间传值的文章。

    2、我们在命令行中执行“clang -rewrite-objc 文件名”便可以将你使用block的文件进行 block 语法转换,得到 一个cpp 这个文件。由于我用的是ViewController.m文件,其中的UIKit头文件转换cpp会报错,所以我就从其他地方直接把转换完成的重要代码直接粘过来了。

     1 struct __block_impl {
     2     void *isa;
     3     int Flags;
     4     int Reserved;
     5     void *FuncPtr;
     6 };
     7  
     8 struct __outside_block_impl_0 {
     9     struct __block_impl impl;
    10     struct __outside_block_desc_0* Desc;
    11     __outside_block_impl_0(void *fp, struct __outside_block_desc_0 *desc, int flags=0) {
    12         impl.isa = &_NSConcreteGlobalBlock;
    13         impl.Flags = flags;
    14         impl.FuncPtr = fp;
    15         Desc = desc;
    16     }
    17 };
    18  
    19 static void __outside_block_func_0(struct __outside_block_impl_0 *__cself) {
    20     printf("Hello block!
    ");
    21 }
    22  
    23 static struct __outside_block_desc_0 {
    24     size_t reserved;
    25     size_t Block_size;
    26 } __outside_block_desc_0_DATA = {
    27     0,
    28     sizeof(struct __outside_block_impl_0)
    29 };
    30  
    31 int main () {
    32     ((void (*)(__block_impl *))((__block_impl *)outside)->FuncPtr)((__block_impl *)outside);
    33     return 0;
    34 }

    先看19-21行的代码,这是一个C语言函数,也就是调用block的时候执行的函数,也就是说block调用在底层其实就是C函数调用。编译器会将block内部代码生成对应的函数。

    再看其他结构也都包含在8-17行的结构体中,block的本质就是这个结构体,所以说block变量的本质是指向结构体的指针,而其中的FuncPtr就是指向block执行函数的函数指针,block执行的时候通过指向这个结构体的指针从而进一步找到指向执行函数的函数指针,再通过这个函数指针便能找到函数的执行位置去执行我们想要block执行的代码。

    如果还想再深入了解block内部结构可以参考一下这篇博客:http://www.cnblogs.com/fengmin/p/5801517.html

    下面再看一下我们使用block的时候遇到的一些关于内存管理的问题:

    首先看一下这段代码:

    1 NSUInteger a = 5;
    2     
    3     void (^block)() = ^{
    4         a = 6;
    5     };

    再看一下这段代码:

    1  NSUInteger a = 5;
    2     
    3     void (^block)() = ^{
    4         NSLog(@"%ld",a);
    5     };
    6     a = 6;
    7     block();

    第一段代码在Xcode中编译会直接报错,而第二段代码你会发现对a的修改是不生效的。这是为什么呢?

    这里的原因我们同样可以通过探究底层的C语言实现来明白。我们可以先来看一下C语言代码:

     1 {
     2    int a = 5;
     3     changeMyNo0(a);
     4     NSLog(@"0:%d",a);
     5     changeMyNo1(&a);
     6     NSLog(@"1:%d",a);
     7 }
     8 
     9 void changeMyNo0(int a) {
    10     a = 6;
    11 }
    12 void changeMyNo1(int *a) {
    13     *a = 7;
    14 }

    这是一段很简单的C语言代码,第一个函数是通过传值尝试改变变量的值,运行之后会发现变量的值不会发生改变;而第二个函数是通过传址来改变变量的值,运行之后会发现改变成功。这是因为第一个函数没有改动变量a本身,而只是改变了它的复制品;而第二个函数通过变量a的地址找到a在内存中的存储位置直接改变了a的值。

    上面的答案和这里是很类似的,我们通过上面的介绍可以知道block中的代码会存到一个函数中,而变量a就是通过传值的方式而不是传址的方式传入这个函数中的,而且变量a传入这个函数之后相当于一个常量,我们知道常量是不能被修改的,因此第一段代码会报错;同时,这个常量相当于a的一个复制品,即使你再次修改了a的值,这个复制品常量的值也是不会改变的。

    下面简单修改一下上面的代码:

    1 __block NSUInteger a = 5;
    2 
    3     void (^block)() = ^{
    4         NSLog(@"%ld",a);
    5     };
    6     a = 6;
    7     block();

    我们在变量前加了一个修饰符__block,再试一下会发现修改成功了,这样就不用多说了,这个修饰符的作用就是将之前的传值变成了传址。

    当然如果是静态变量或者全局变量的话,也是可以修改成功的,因为这种情况全局操作中只有一份这个变量,所以对这个变量的任何改变都是对这个变量本身的改变。

    下面看一下block在ARC和非ARC环境下的内存管理的一些问题:

    非ARC:

     1 __block Person *p = [[Person alloc] init]; // 1
     2     p.age = 20;
     3 
     4     void (^block)() = ^{
     5         NSLog(@"------block------%d", p.age);
     6     };
     7     NSLog(@"%zd", [p retainCount]);
     8     Block_copy(block); // 2
     9     NSLog(@"%zd", [p retainCount]);
    10 
    11     [p release]; // 1

    * 默认情况下, block的内存是在栈中的,它不会对所引用的对象进行任何操作;因此,如果去掉上面代码中的第8行,p对象能被顺利释放,调用dealloc方法。否则,调用了第8行代码,block的内存会被存到堆中,所以会增加它对内部对象的引用,也就是说p的retainCount会加1,这时候如果没有__block修饰符的话,便不会顺利释放,因为中间p的引用计数被加了1;

    * 这就说道__block修饰符在这里的作用:如果对block做一次copy操作, block的内存就会在堆中;它会对所引用的对象做一次retain操作

    * ARC : 如果所引用的对象用了__block修饰, 就不会做retain操作。

    * ARC : 如果所引用的对象用了__unsafe_unretained\__weak修饰, 就不会做retain操作。

     

    ARC:

    1 @property (nonatomic, copy) void (^block)();
    1  __weak typeof(self) dog = self;
    2  self.block = ^{
    3      [dog run];
    4  };

    因为用了copy策略,所以如果不加__weak修饰,会形成强引用循环。

    demo:https://github.com/alan12138/Interview-question/tree/master/2/block%E7%9A%84%E4%BD%BF%E7%94%A8%E4%BB%A5%E5%8F%8A%E5%AE%9E%E7%8E%B0%E5%8E%9F%E7%90%86

     

     

  • 相关阅读:
    ObjectC&&Swift 渐变色算法实现
    【iOS数据存储】iOS文件系统介绍
    1 、Quartz 2D绘图基础
    iOS 常用框架列表
    【Foundation Frame】Struct
    【Foundation Frame】NSMutableArray
    【Foundation Frame】NSDictionary/NSMutableDictionary
    【Foundation Frame】NSString
    【Foundation Frame】NSArray
    在vue项目中使用自己封装的ajax
  • 原文地址:https://www.cnblogs.com/alan12138/p/5804031.html
Copyright © 2011-2022 走看看