2019年年初iOS招人心得笔记 答案 (一)

这是题目
http://www.cocoachina.com/cms/wap.php?action=article&id=26253

技术基础

1、我们说的Objective-C是动态运行时语言是什么意思?

OC的动态特性表现为三个方面:动态类型、动态绑定、动态加载。

(1)动态类型——id
实际上静态类型因为其固定性和可预知性而得到更广泛的应用。静态类型是强类型,而动态类型属于弱类型,运行时决定接收者。

这里解释一下强、弱类型:语言有无类型、强类型和弱类型三种。无类型的不做任何检查,甚至不区分指令和数据。弱类型的检查很弱,仅区分指令和数据。强类型在编译器进行严格检查,强类型语言在没有强制类型转化前,不允许两种不同类型的变量相互操作。

(2)动态绑定——@selector/SEL
让代码在运行的时候判断需要调用的方法,而不是在编译时。与其他面向对象语言一样,方法调用和代码并没有在编译时连接在一起啊,而是在消息发送时才进行链接。运行时决定调用哪个方法。

(3)动态加载
让程序在运行时添加代码模块和资源的时候,用户可以根据需要加载一些可执行代码和资源,而不是在启动时就加载所有组件。可执行代码中可以含有程序运行时整合的新类。

2、讲一下MVC和MVVM,MVP?

  • MVC
    MVC分别代表Model,View,Controller。特点是M和V相互隔离,通过C进行通信。
    这是苹果文档中对于MVC的解释
    https://developer.apple.com/library/archive/documentation/General/Conceptual/DevPedia-CocoaCore/MVC.html
2019年年初iOS招人心得笔记 答案 (一)_第1张图片
Model-View-Controller

MVC的缺点:

(1)越来越笨重的Controller

控制器Controller是app的胶水代码,协调Model和VIew之间的交互。控制器负责管理视图层次结构,还要响应视图的loading、appearing、disappearing等等。同时往往也会充满我们不愿意暴露Model的模型逻辑和不愿意放在Controller的视图业务逻辑。

在实际开发中,ViewController同时扮演了C和部分V的角色,显得非常庞大,其中包含了各种业务和视图层逻辑,不利于测试。


Massive ViewController.png

(2)太过于轻量化的Model

Model层就是几个属性,ARC普及之后,m文件中基本看不到代码。同时和控制器代码越来越厚形成强烈反差

(3)遗失的网络逻辑

苹果使用的MVC的定义是这么说的:所有的对象都可以归类为Model、View、Controller。就这样的话,网络逻辑放置在哪里就很棘手。

你可以试着把网络放在Model对象里,但是很棘手,因为网络调用应该使用异步,如果一个网络请求比持有它的Model生命周期更长,事情将变得复杂。显然也不应该把网络代码放在View里,因此只能放在控制器了。这也不是不能接收的,但是这样会加厚控制器。

(4)较差的可测试性

MVC的一个大问题是控制器混合了视图逻辑和业务逻辑,分离视图和业务并写单元测试成为一个艰巨的任务。大多数人选择不做任何测试。

  • MVP

Model View Presenter(协调器)

2019年年初iOS招人心得笔记 答案 (一)_第2张图片
MVP

MVP中的Presenter并没有对ViewController的生命周期做任何改变,因此View可以很容易测试。在Presenter中主要负责更新View的数据和状态,并没有布局相关的代码。

MVP特点:

  • 职责拆分——我们将主要的任务分到Presenter和Model中,View的功能减少
  • 可测试——基于功能简单的View层,可以测试大部分业务逻辑
  • 易用性——MVP各代码结构职责清晰

MVP优势:
模型和视图完全分离,我们可以修改视图而不影响模型
我们可以把逻辑放在Presenter中,那么我们可以脱离用户借口来测试这些逻辑

  • MVVM
2019年年初iOS招人心得笔记 答案 (一)_第3张图片
MVVM

VIewModel是一个防止用户输入验证逻辑,视图展示逻辑,发起网络请求和其他各种各样代码的地方。

3、为什么代理要用weak?代理的delegate和dataSource有什么区别?block和代理的区别?

防止代理和被代理对象相互强引用造成内存泄漏。

delegate主要负责view的交互 datasource主要负责提供数据

block是一个对象,代理是一个设计模式。

代理的好处:
delegate运行成本低,block成本高。
block出栈需要将使用的数据从栈内存拷贝到堆内存,使用完成后还需要销毁block。delegate只是保存了一个对象指针,直接回调,没有额外的消耗。相对于C,只多了一个查表的操作。

block:
写法简单,不需要写protocol、函数等等
block需要防止循环引用

什么时候用代理或者block
公共借口,方法比较多可以选择用delegate进行解耦
异步和简单的回调用block比较好

4、属性的实质是什么?包括哪几个部分?属性默认的关键字都有哪些?@dynamic关键字和@synthesize关键字是用来做什么的?

属性的实质是什么?包括哪几个部分?
@property = ivar(实例变量) + getter + setter
利用class_copyPropertyList查看类的属性
利用class_copyIvarList查看类的所有成员变量
利用class_copyMethodList查看类的所有方法

属性默认的关键字都有哪些?

  • 原子性 nonatomic atomic。当自己没有指定原子性的时候,默认是atomic,并且在自己定义存取方法,应该遵守与属性特质相符的原子性。
  • 读写权限 readwrite readonly。
  • 内存管理语意 assign strong weak unsafe_unretained copy。assign只针对纯量类型(scalar type)的简单赋值操作。unsafe_unretained中当目标对象销毁之后属性只不会清空。
  • 方法名 setter getter。

@dynamic关键字和@synthesize关键字是用来做什么的?

  • @synthesize可以用来指定实例变量的名称
  • @dynamic告诉编译器不要自动生成属性所用的实例变量,也不要为其创建存取方法,自己进行手动管理。

5、属性的默认关键字是什么?

  • 原子性 默认是atomic
  • 读写权限 默认是readwrite
  • 内存管理语意 纯量变量是assign 对象是strong
  • 方法名 无

6、NSString为什么要用copy关键字,如果用strong会有什么问题?(注意:这里没有说用strong就一定不行。使用copy和strong是看情况而定的)

主要是防止属性被污染,当把一个可变(nsmutable)对象赋值给strong的属性时,改变对象可能导致属性发生改变。

如果想在model内直接实时反应数据的变化就使用strong,要防止数据更改变化使用copy。

7、如何令自己所写的对象具有拷贝功能?

自定义对象分为可变版本和不可变版本,需要同时实现NSCopying和NSMutablecopying协议

@protocol NSCopying

- (id)copyWithZone:(nullable NSZone *)zone;

@end

@protocol NSMutableCopying

- (id)mutableCopyWithZone:(nullable NSZone *)zone;

@end

8、简述kvo、kvc、Delegate他们之间的区别?

KVC(key value coding)键值编码,iOS开发中,可以允许开发者通过key名直接访问对象属性,或者给对象的属性赋值。而不需要调用明确的存取方法。这样就可以在运行时动态的访问和修改对象的属性。而不是编译时确定。

KVC的定义都是对NSObject的扩展来实现的,OC中有个显式的NSKeyValueCoding类别名,所以对于所有继承了NSObject的类型,都能使用KVC(一些纯swift类和结构体是不支持KVC的,因为没有继承NSObject),下面是KVC最为重要的四个方法:

- (nullable id)valueForKey:(NSString *)key;                          //直接通过Key来取值

- (void)setValue:(nullable id)value forKey:(NSString *)key;          //通过Key来设值

- (nullable id)valueForKeyPath:(NSString *)keyPath;                  //通过KeyPath来取值

- (void)setValue:(nullable id)value forKeyPath:(NSString *)keyPath;  //通过KeyPath来设值

NSKeyValueCoding类别中其他的一些方法:

+ (BOOL)accessInstanceVariablesDirectly;
//默认返回YES,表示如果没有找到Set方法的话,会按照_key,_iskey,key,iskey的顺序搜索成员,设置成NO就不这样搜索

- (BOOL)validateValue:(inout id __nullable * __nonnull)ioValue forKey:(NSString *)inKey error:(out NSError **)outError;
//KVC提供属性值正确性验证的API,它可以用来检查set的值是否正确、为不正确的值做一个替换值或者拒绝设置新值并返回错误原因。

- (NSMutableArray *)mutableArrayValueForKey:(NSString *)key;
//这是集合操作的API,里面还有一系列这样的API,如果属性是一个NSMutableArray,那么可以用这个方法来返回。

- (nullable id)valueForUndefinedKey:(NSString *)key;
//如果Key不存在,且没有KVC无法搜索到任何和Key有关的字段或者属性,则会调用这个方法,默认是抛出异常。

- (void)setValue:(nullable id)value forUndefinedKey:(NSString *)key;
//和上一个方法一样,但这个方法是设值。

- (void)setNilValueForKey:(NSString *)key;
//如果你在SetValue方法时面给Value传nil,则会调用这个方法

- (NSDictionary *)dictionaryWithValuesForKeys:(NSArray *)keys;
//输入一组key,返回该组key对应的Value,再转成字典返回,用于将Model转到字典。

KVO 即 Key-Value Observing,翻译成键值观察。它是一种观察者模式的衍生。其基本思想是,对目标对象的某属性添加观察,当该属性发生变化时,通过触发观察者对象实现的KVO接口方法,来自动的通知观察者。

简单来说KVO可以通过监听key,来获得value的变化,用来在对象之间监听状态变化。KVO的定义都是对NSObject的扩展来实现的,OC中有个显式的NSKeyValueObserving类别名,所以对于所有继承了NSObject的类型,都能使用KVO(纯swift类和结构体是不支持KVC的,因为没有继承NSObject)。

KVO的实现依赖于Runtime的强大动态能力。

即当一个类型为ObjectA的对象,被添加观察后,系统会生成一个NSKVONotifying_ObjectA类,并将对象的isa指针指向新的类,也就是说这个对象的类型发生了变化。这个类相对于ObjectA,会重写

setter方法

- (void)willChangeValueForKey:(NSString *)key;
- (void)didChangeValueForKey:(NSString *)key;

然后在didChangeValueForKey:中,去调用:

- (void)observeValueForKeyPath:(nullable NSString *)keyPath
                      ofObject:(nullable id)object
                        change:(nullable NSDictionary *)change
                       context:(nullable void *)context;

包括了新值和旧值的通知。因为KVO的原理是修改setter方法,因此使用KVO必须调用setter。若直接访问属性对象则没有效果。

重写class

当修改了isa指向后,class的返回值不会变,但isa的值则发生改变。

重写dealloc

系统重写dealloc方法释放资源

重写_isKVOA

这个私有方法是用来标示该类是一个KVO机制声称的类。

Delegate

顾名思义委托别人办事,就是当一件事情发生之后,自己不去处理,让被人来处理。

一般委托者需要做的事:

  1. 创建协议(也就是代理需要实现的方法)
  2. 声明委托变量(也就是delegate属性)
  3. 设置代理(也可以在代理对象中设置)
  4. 利用委托变量调用协议方法(也就是让代理着开始执行协议)

代理对象需要做的事:

  1. 遵守协议
  2. 实现协议方法

9、#include与#import的区别?#import与@class的区别?

import可以防止#include引起的循环引用

import会包含这个类的全部信息,包括实体变量和方法(.h文件),而@class只能告诉编译器,其后面声明的名称是类的名称,至于这个类是如何定义的,后面会有。所以一般在头文件中使用@class来声明这个名称,在类实现里面,使用#import引用实体变量和方法。

10、nonatomic和atomic的区别?atomic是绝对的线程安全么?为什么?如果不是,那应该如何实现?

atomic:原子属性,多线程写入属性时,同一时间只能有一个线程能够写入操作。
atomic内部有一个互斥锁

什么是自旋锁和互斥锁

锁用于解决线程争夺资源的问题,一般分为两种,自旋锁(spin)和互斥锁(mutex)。

互斥锁可以解释为线程获取锁,发现锁被占用,就向系统申请锁空闲时唤醒他并立刻休眠。

自旋锁比较简单,当线程发现锁被占用时,会不断循环判断锁的状态,直到获取。

原子操作的颗粒度最小,只限于读写,对于性能的要求很高,如果使用了互斥锁势必在切换线程上耗费大量资源。相比之下,由于读写操作耗时比较小,能够在一个时间片内完成,自旋更适合这个场景。

自旋锁的坑

但是iOS 10之后,苹果因为一个巨大的缺陷弃用了 OSSpinLock 改为新的 os_unfair_lock。

新版 iOS 中,系统维护了 5 个不同的线程优先级/QoS: background,utility,default,user-initiated,user-interactive。高优先级线程始终会在低优先级线程前执行,一个线程不会受到比它更低优先级线程的干扰。这种线程调度算法会产生潜在的优先级反转问题,从而破坏了 spin lock。

描述引用自 ibireme 大神的文章。

我的理解是,当低优先级线程获取了锁,高优先级线程访问时陷入忙等状态,由于是循环调用,所以占用了系统调度资源,导致低优先级线程迟迟不能处理资源并释放锁,导致陷入死锁。

当使用atomic时,虽然对属性的读和写是原子性的,但是仍然可能出现线程错误:当线程A进行写操作,这时其他线程的读或者写操作会因为等该操作而等待。当A线程的写操作结束后,B线程进行写操作,所有这些不同线程上的操作都将依次顺序执行——也就是说,如果一个线程正在执行 getter/setter,其他线程就得等待。如果有线程C在A线程读操作之前release了该属性,那么还会导致程序崩溃。所以仅仅使用atomic并不会使得线程安全,我们还要为线程添加lock来确保线程的安全。

更准确的说应该是读写安全,但并不是线程安全的,因为别的线程还能进行读写之外的其他操作。线程安全需要开发者自己来保证。

其实无论是否是原子性的只是针对于getter和setter而言,比如用atomic去操作一个NSMutableArray ,如果一个线程循环读数据,一个线程循环写数据,肯定会产生内存问题,这个就跟getter和setter就木有关系了。

这里写一下在MRC下的setter/getter代码实现

static NSString *const objLock = @"objLock";

@interface ViewController ()
@property (retain, atomic) NSObject *obj;
@end

@implementation ViewController
- (void)setObj:(NSObject *)obj {
    @synchronized(objLock) {
        [obj retain];
        [_obj release];
        _obj = obj;
    }
}

- (NSObject *)obj {
    @synchronized(objLock) {
        return _obj;
    }
}
@end
为什么不能保证绝对的线程安全?

单独的原子操作绝对是线程安全的,但是组合一起的操作就不能保证。

- (void)competition {
    self.intSource = 0;

    dispatch_async(queue1, ^{
      for (int i = 0; i < 10000; i++) {
          self.intSource = self.intSource + 1;
      }
    });

    dispatch_async(queue2, ^{
      for (int i = 0; i < 10000; i++) {
          self.intSource = self.intSource + 1;
      }
    });
}

最终得到的结果肯定小于20000。当获取值的时候都是原子线程安全操作,比如两个线程依序获取了当前值 0,于是分别增量后变为了 1,所以两个队列依序写入值都是 1,所以不是线程安全的。

解决的办法应该是增加颗粒度,将读写两个操作合并为一个原子操作,从而解决写入过期数据的问题。

os_unfair_lock_t unfairLock;
- (void)competition {
    self.intSource = 0;

    unfairLock = &(OS_UNFAIR_LOCK_INIT);
    dispatch_async(queue1, ^{
      for (int i = 0; i < 10000; i++) {
          os_unfair_lock_lock(unfairLock);
          self.intSource = self.intSource + 1;
          os_unfair_lock_unlock(unfairLock);
      }
    });

    dispatch_async(queue2, ^{
      for (int i = 0; i < 10000; i++) {
          os_unfair_lock_lock(unfairLock);
          self.intSource = self.intSource + 1;
          os_unfair_lock_unlock(unfairLock);
      }
    });
}

参考文章
https://www.jianshu.com/p/740ec0c85e97
https://blog.ibireme.com/2016/01/16/spinlock_is_unsafe_in_ios/

你可能感兴趣的:(2019年年初iOS招人心得笔记 答案 (一))