一.ObjC的属性特性
1). assign:赋值特性,setter方法将入参赋值给实例变量;常用于设置纯量类型变量(如int,float,bool)时。
2). retain:保留特性,setter方法将入参先保留,再赋值,入参的引用计数会+1;常用于修饰大部分OC对象。
3).unsafe_unretained(ARC下):非安全且非持有特性,与assign等价;可以被用来修饰对象,但容易引发野指针问题而导致Crash,不常用。
4).weak(ARC下):非持有特性,setter方法设置新值时仅将入参赋值给实例变量,特性与assign类似,但所指对象遭到销毁时会指向nil,从而避免出现野指针,故常用于修饰可能出现循环引用的对象(如代理模式中的delegate指针)。
5).strong (ARC下):持有特性,setter方法将入参先保留,再赋值,入参的引用计数会+1,与retain类似;常用于修饰大部分OC对象。
6).copy:拷贝特性,setter方法将入参复制一份,复制的对象和原对象的引用计数不变;如果希望属性变量不随入参变化而变化,则常用该属性修饰,如NSString和Block对象。
7).nonatomic:非原子特性,决定编译器生成的setter和getter是否是原子操作,若具备该特性,则不使用同步锁,atomic表示多线程安全,app开发一般使用nonatomic。
延伸问题:
strong和retain有什么区别?
在使用strong或retain设置新值时,保留、释放、赋值三个操作的顺序应当如何?
copy和strong有什么区别?为什么NSString和Block对象要使用copy?
自定义对象如何实现copy特性?
二.简述iOS开发中常用的架构模式、设计模式
常用架构模式
架构模式的出现是为了管理复杂的应用程序,这样可以在特定时间内专门关注一个方面。例如你可以在不依赖业务逻辑的情况下专注于视图设计。同时也让应用程序的测试更加容易,简化了分组开发,使得不同的开发人员可同时开发视图、控制器逻辑和业务逻辑。我们经常说的MVC、MVP、MVVM架构便属于此范畴内。但无论是何种架构模式,都共同遵循“高内聚低耦合”思想,开发中需要根据实际情况采用合适的架构思路。
- MVC:字面拆分开即Model(模型)、 View (视图) 、Controller(控制器)的架构层次,三者各司其职。iOS下的MVC,Model主要负责数据的解析和存取,仅仅是一个简单的数据模型,往往不包含其他业务逻辑,View依赖Model呈现给用户不同数据并传递用户事件,Controller负责响应用户事件,同时充当View和Model之间的媒介,此外还负责业务逻辑,如管理其他对象的生命周期、网络请求、通知等等。
- MVP:苹果推崇的MVC模式,因为UIView和UIViewController本身已经耦合,这就引发了一个难以规避的问题:Controller将变得异常臃肿。因此当界面足够复杂时,为Controller减重成了首要考虑的问题。MVP(Model-View-Presenter)可以较好的解决此问题。将Controller和View合并成View,Model负责网络请求,数据库操作,数据封装等业务逻辑,新增Presenter模块负责View和Model桥接工作以及View的响应事件等业务逻辑。与MVC相比,Controller原本的职责被分配到Presenter和Model中,它只需要管理View的生命周期即可。在轻Model的MVP中,Model的职责会大大减少,Presenter的任务会加重。
- MVVM: 移动开发往往热衷于轻Model的架构模式,因为模型层很少直接和数据源直接交互,中间还有复杂的服务层。 出于业务考虑,有时候Model层的改变可能需要直接引起View层的变化,View层的改变也需要改变Model层,Model不仅仅是简单的数据模型,两者之间可能存在复杂的绑定关系,加上Model的数据可能会在多个页面使用,因此还要考虑Model的复用,将这些绑定、复用逻辑全都放ViewModel中而取代Presenter,即MVVM(Model-View-ViewModel)
常用设计模式
设计模式可以通俗的理解为实现/解决某些问题,而形成的解决方案规范,可以增加代码的可重用性,让代码能更容易理解和可靠。我们通常说所的代理模式、迭代器模式、策略模式就属于该范畴。对各种设计模式的了解可以帮助我们更快的解决编程过程中遇到的问题。
- 委托模式:代理+协议的组合,实现1对1的反向传值操作。
- 观察者模式: 当被观察者的某一属性发生改变时,所有依赖于它的观察者都能够响应。观察者模式不需要向被观察者添加额外的代码,因此能够较美地将目标对象与观察者对象解耦。iOS中的KVO,通知都是基于观察者模式。
- 简单工厂模式(非23种设计模式):通过一个工厂类方法,批量的根据已有模板生产目标对象。
- 工厂方法模式:通过多个工厂类方法,批量的根据已有模板生产目标对象。
- 抽象工厂模式:通过一个工厂类方法,批量的根据已有模版生产工厂对象,再由工厂对象创建目标对象。
- 单例模式:保证一个类仅有一个实例,并提供一个访问它的全局访问点。比如NSUserDefaults、UIApplication、NSBundle、NSFileManager、NSNotificationCenter、NSRunLoop等
- 迭代器模式::提供一种方法顺序的访问一个聚合对象中各个元素,而又不暴露该对象的内部表示。
- 策略模式:定义一系列的算法,把它们一个个封装起来, 并且使它们可相互替换。本模式使得算法的变化可独立于使用它的客户。
- ...
延伸问题:
MVC架构模式,如何为C层减重?
使用代理协议与使用Block进行对象间通信,各有什么特点?
三种工厂模式有什么区别?oc中使用的类族属于哪种工厂模式?
设计模式有哪三大类型?上述设计模式分别属于哪一类?
三.简述NSRunLoop
NSRunLoop是苹果封装的一个消息循环类。每一个NSRunLoop实例对象,可以看作一个While循环。退出该循环的条件是收到某条指令消息,否则程序将一直保持运行。当app运行后,主线程就处于一个NSRunLoop之中,因此主线程可以持续的接收用户事件,而杀死app则可以视为退出While循环的指令。当然,实际情况要复杂得多,但明确的是,RunLoop是为线程服务,与线程密不可分,一一对应。对于RunLoop,需要明确以下几点:
- 主线程的RunLoop默认启动。程序运行后,在入口main函数中,会为主线程设置一个NSRunLoop对象。
- 对其它线程来说,RunLoop默认不启动,如果子线程需要一直处于循环状态则可以手动配置和启动,如果子线程仅仅执行一个长时间的异步任务则不需要。
- RunLoop需要处理多种消息源,因此有多种模式用于处理各种消息。常用的有kCFRunLoopDefaultMode、UITrackingRunLoopMode、UIInitializationRunLoopMode:、NSRunLoopCommonModes
延伸问题:
为什么定时器需要加入到RunLoop之中?
NSRunLoop几种模式的区别?有哪些使用场景?
NSRunLoop的生命周期?
四.简述RunTime
Runtime 即运行时,是一套底层的 C 语言 API。也是Objective-C作为动态语言的核心技术。能够在程序运行期,动态的创建类,遍历一个类中所有的成员变量、属性、方法,动态的修改某个类的属性和方法,开发中主要体现在以下场景:
- Category
- Method-Swizzing
- 获取对象私有属性
- 字典转模型
- KVC
- KVO
- 反射机制
- 消息机制
- ...
五.简述iOS消息机制
在Objc底层,所有方法都是普通的C语言函数,Objc中的方法又称作消息,但消息究竟对应哪个C函数则完全于运行期决定,甚至可以在运行期动态改变。消息机制由消息传递机制和消息转发机制两部分组成。
- 消息传递机制:
id returnValue = [someObject messageName:parameter]
//someObject是接受者(receiver),messageName是选择子(selector),选择子和参数合起来称为消息(message)。
//编译器看到此消息后,将其转换为如下标准的c函数,此函数乃是消息传递机制中的核心函数
void objc_msgSend(id self, SEL cmd, ...)
objc_msgSend函数会依据接受者与选择子的类型继续调用适当的方法,该方法会在接受者所属的类中搜寻其“方法列表”。找到则跳到真正的实现代码,否则,就沿着继承体系继续向上查找,这一流程叫做消息传递。消息传递结束仍找不到具体实现代码,则执行消息转发操作。
- 消息转发机制:消息转发是在消息传递后,依然找不到实现代码时启动的一套补救措施。开发者可以通过实现重写NSObject类的以下方法,利用Runtime API在运行期新增方法做一些特殊处理。
//能否动态添加实例方法
+ (BOOL)resolveInstanceMethod:(SEL)selector
//能否动态添加类方法
+ (BOOL)resolveClassMethod:(SEL)selector
//能否由其他接受者处理此消息
- (id)forwardingTargetForSelector:(SEL)selector
//最终转发消息的方法,如果此方法失败,则会跑抛出异常Crash,即常见的unrecognized selector send to instance xxx
- (void)forwardInvocation:(NSInvocation *)invocatio
延伸问题:
是否针对每条消息,都会经历如此复杂的一套机制?
六.简述多线程编程
iOS中常用的多线程技术主要有GCD(Grand Central Dispatch)、NSThread、NSOperation三种,他们都可以用来处理多线程任务。其中以GCD最为简单方便,也是苹果主要推荐的方式。此处对GCD做一些简单介绍。
GCD的核心概念:
- 任务:Block中所要执行的代码。
- 队列:用于存放任务的线性表,采用先进先出原则。有串行队列和并发队列两种。
- 串行队列:每次只有一个任务被执行。任务一个接着一个地执行。(只开启一个线程,一个任务执行完毕后,再执行下一个任务)
- 并发队列:可以让多个任务并发(同时)执行。(可以开启多个线程,并且同时执行任务)
- 主队列:系统提供的默认串行队列
- 全局并发队列:系统提供的默认并发队列
- 同步执行(sync):同步添加任务到指定队列,会等待队列前面的任务执行完毕,再继续执行,只能在当前线程中执行,不具备开启新线程能力。
- 异步执行(async):异步添加任务到指定的队列,不做任何等待,继续执行任务,可以在新的线程中执行任务,具备开启新线程的能力。
从以上概念可以得出结论:一个任务是否在新线程中执行,由两方面决定:同步还是异步,串行还是并发。
GCD常用的API:
// 串行队列的创建方法
dispatch_queue_t queue = dispatch_queue_create("testQueue", DISPATCH_QUEUE_SERIAL);
// 并发队列的创建方法
dispatch_queue_t queue = dispatch_queue_create("testQueue", DISPATCH_QUEUE_CONCURRENT);
// 主队列的获取方法
dispatch_queue_t queue = dispatch_get_main_queue();
// 全局并发队列的获取方法
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
// 同步执行任务创建方法
dispatch_sync(queue, ^{
/// 这里放同步执行任务代码,queue可以是任何队列
});
// 异步执行任务创建方法
dispatch_async(queue, ^{
/// 这里放异步执行任务代码,queue必须要是并行队列
});
//栅栏分组方法
dispatch_barrier_async(queue, ^{
/// 栅栏追加任务,queue必须是并行队列
});
//主队列延时调用方法
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
/// 2.0秒后追加任务到主队列
});
// 只执行一次方法
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
/// 只执行1次的代码(这里面默认是线程安全的)
});
延伸问题:
GCD什么时候会出现死锁?如何解决?
GCD信号量的用处?
七.简述iOS的锁机制
多线程开发中,当至少有两个线程同时访问同一个变量,而且至少其中有一个是写操作时,就发生了数据竞争(Data race)。所以这是就要利用一些同步机制来确保数据的准确性,锁就是同步机制中的一种。
锁的基本概念:
- 临界区:指的是一块对公共资源进行访问的代码,并非一种机制或是算法。
- 自旋锁:是用于多线程同步的一种锁,线程反复检查锁变量是否可用。由于线程在这一过程中保持执行,因此是一种
忙等待
。一旦获取了自旋锁,线程会一直保持该锁,直至显式释放自旋锁。 自旋锁避免了进程上下文的调度开销,因此对于线程只会阻塞很短时间的场合是有效的。 - 互斥锁(Mutex):是一种用于多线程编程中,防止两条线程同时对同一公共资源(比如全局变量)进行读写的机制。该目的通过将代码切片成一个一个的临界区而达成。
- 读写锁:是计算机程序的并发控制的一种同步机制,也称“共享-互斥锁”、多读者-单写者锁) 用于解决多线程对公共资源读写问题。读操作可并发重入,写操作是互斥的。 读写锁通常用互斥锁、条件变量、信号量实现。
- 信号量(semaphore):是一种更高级的同步机制,
互斥锁
可以说是semaphore在仅取值0/1时的特例。信号量
可以有更多的取值空间,用来实现更加复杂的同步,而不单单是线程间互斥。 - 条件锁:条件变量,当进程的某些资源要求不满足时就进入休眠,也就是锁住了。当资源被分配到了,条件锁打开,进程继续运行。
- 递归锁:同一个线程可以加锁N次而不会引发死锁的同步机制
iOS框架中的十一种常用锁:
- NSLock:互斥锁,是Foundation框架中以对象形式暴露给开发者
- pthread_mutex:互斥锁
- @synchronized:互斥锁
- OSSpinLock:自旋锁,在某一些场景下已经不安全了,使用较少
- os_unfair_lock:自选锁,苹果官方推荐的替换OSSpinLock的方案(iOS10+)
- pthread_rwlock:读写锁
- NSRecursiveLock:递归锁
- pthread_mutex(recursive)::递归锁,pthread_mutex的变形
- NSCondition:条件锁,需遵循NSLocking协议
- NSConditionLock:条件锁,基本同上
- dispatch_semaphore:信号量,GCD中的线程同步机制
八.简述分类(Category)和扩展(Extension)
Category:在运行期为类动态的添加方法。也可以添加属性,但系统不会生成实例变量。需自己实现setter和getter,并使用Runtime API动态添加成员变量。
Extension:在编译期为类添加成员变量(属性)。通常在.m文件内实现,因此又叫匿名分类,但和Category的实现原理有本质区别。
九.简述Method-wizzing
Method-Swizzing是OC对AOP编程的实现,AOP(面向切面编程)指的是通过‘预编译’和‘运行期’实现在不修改源代码的情况下给程序动态统一添加功能的一种技术。OC的Runtime库提供了一个叫method_exchangeImplementations的api,正如其名,这个方法可以将两个方法互换,因为OC调用函数是通过消息机制,在执行最终的函数之前,会通过SEL(方法序列)对应函数指针IMP,因此将两个SEL交换,变可以将系统方法和自己的方法进行交换。
延伸问题:
分类、继承、Method-Swizzing相比,各有何特点?