Wrappres' Studio.

Interview Questions

字数统计: 9.4k阅读时长: 34 min
2021/06/09 Share

分类和扩展有什么区别?可以分别用来做什么?分类有哪些局限性?分类的结构体里面有哪些成员?

分类

通常用于将一个功能强大臃肿的类,分布在几个不同的文件。

只能增加方法,不能增加成员(实例)变量。

1
2
3
4
5
6
7
8
9
Category 是表示一个指向分类的结构体的指针,其定义如下:
typedef struct objc_category *Category;
struct objc_category {
char *category_name OBJC2_UNAVAILABLE; // 分类名
char *class_name OBJC2_UNAVAILABLE; // 分类所属的类名
struct objc_method_list *instance_methods OBJC2_UNAVAILABLE; // 实例方法列表
struct objc_method_list *class_methods OBJC2_UNAVAILABLE; // 类方法列表
struct objc_protocol_list *protocols OBJC2_UNAVAILABLE; // 分类所实现的协议列表
}

可以通过runtime实现增加属性

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#import "People+Work.h"
#import <objc/runtime.h>

static NSString *skillKey = @"skillKey";

@implementation People (Work)

- (void)setSkill:(NSString *)skill {
objc_setAssociatedObject(self, &skillKey, skill, OBJC_ASSOCIATION_COPY);
}

- (NSString *)skill {
return objc_getAssociatedObject(self, &skillKey);
}
@end

以上方法仅仅增加了get set方法 成员变量_skill并没有增加直接调用会报错

扩展

匿名的分类,不仅可以增加方法,也能增加私有属性。

没有独立的implementation部分。

定义在 .m 文件中的类扩展方法为私有的,定义在 .h 文件(头文件)中的类扩展方法为公有的。类扩展是在 .m 文件中声明私有方法的非常好的方式。

讲一下atomic的实现机制;为什么不能保证绝对的线程安全(最好可以结合场景来说)?

atomic 的作用是在set get方法中添加spinlock_t(自旋锁)来保证数据仅被一个线程占用。

线程安全是指在多线程环境不出现意想不到的情况,仅靠atomic是无法实现的。

例如

  1. A线程对属性加1,本想立马读取属性,但B线程先对属性减1,导致A线程后来读取到的值错误。
  2. 属性如果是集合类型,addObject等方法并不是线程安全的,属性是类的话也同理。

本质上是atomic是保证了属性的地址值的线程安全,并不是对象层面的线程安全。

被weak修饰的对象在被释放的时候会发生什么?是如何实现的?知道sideTable么?里面的结构可以画出来么?

weak

作用是弱引用,所引用对象的计数器不会加1,并在引用对象被释放的时候自动被设置为 nil。

通过调用storeWeak来添加弱引用。

内部主要如果weak指针之前指向了一个弱引用,则用weak_unregister_no_lock将旧的weak指针地址移除。

接着用weak_register_no_lock把新的weak指针地址添加到弱引用表中。

如果对象没有再析构且可以被weak引用,则调用weak_entry_for_referent方法根据弱引用对象的地址从弱引用表中找到对应的weak_entry,如果能够找到则调用append_referrer方法向其中插入weak指针地址。否则新建一个weak_entry。

对象释放时,调用rootDealloc进行释放。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
inline void
objc_object::rootDealloc()
{
if (isTaggedPointer()) return; // fixme necessary?

if (fastpath(isa.nonpointer &&
!isa.weakly_referenced &&
!isa.has_assoc &&
!isa.has_cxx_dtor &&
!isa.has_sidetable_rc))
{
assert(!sidetable_present());
free(this);
}
else {
object_dispose((id)this);
}
}

如果对象是采用了优化的isa计数方式,且同时满足对象没有被weak引用!isa.weakly_referenced、没有关联对象!isa.has_assoc、没有自定义的C++析构方法!isa.has_cxx_dtor、没有用到SideTable来引用计数!isa.has_sidetable_rc则直接快速释放。

否则调用object_dispose

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
void *objc_destructInstance(id obj) 
{
if (obj) {
// Read all of the flags at once for performance.
bool cxx = obj->hasCxxDtor();
bool assoc = obj->hasAssociatedObjects();

// This order is important.
if (cxx) object_cxxDestruct(obj);
if (assoc) _object_remove_assocations(obj);
obj->clearDeallocating();
}

return obj;
}

调用clearDeallocating函数根据对象地址获取weak指针地址的数组,遍历这个数组把其中的数据设为nil,最后把这个entry从weak表中删除,最后清除这个对象数据。

SideTable

1
2
3
4
5
struct SideTable {
spinlock_t slock;
RefcountMap refcnts;
weak_table_t weak_table;
}

内部主要有三部分:自旋锁,引用计数表,weak表。

weak_table_t

1
2
3
4
5
6
struct weak_table_t {
weak_entry_t *weak_entries;
size_t num_entries;
uintptr_t mask;
uintptr_t max_hash_displacement;
};

内部主要包括:

  • weak_entries:hash数组,存储着弱引用对象的相关信息weak_entry_t

  • num_entries:hash数组中的元素个数

weak_entry_t

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
#define WEAK_INLINE_COUNT 4
#define REFERRERS_OUT_OF_LINE 2

struct weak_entry_t {
DisguisedPtr<objc_object> referent; // 被弱引用的对象

// 引用该对象的对象列表,联合。 引用个数小于4,用inline_referrers数组。 用个数大于4,用动态数组weak_referrer_t *referrers
union {
struct {
weak_referrer_t *referrers; // 弱引用该对象的对象指针地址的hash数组
uintptr_t out_of_line_ness : 2; // 是否使用动态hash数组标记位
uintptr_t num_refs : PTR_MINUS_2; // hash数组中的元素个数
uintptr_t mask; // hash数组长度-1,会参与hash计算。(注意,这里是hash数组的长度,而不是元素个数。比如,数组长度可能是64,而元素个数仅存了2个)素个数)。
uintptr_t max_hash_displacement; // 可能会发生的hash冲突的最大次数,用于判断是否出现了逻辑错误(hash表中的冲突次数绝不会超过改值)
};
struct {
// out_of_line_ness field is low bits of inline_referrers[1]
weak_referrer_t inline_referrers[WEAK_INLINE_COUNT];
};
};

bool out_of_line() {
return (out_of_line_ness == REFERRERS_OUT_OF_LINE);
}

weak_entry_t& operator=(const weak_entry_t& other) {
memcpy(this, &other, sizeof(other));
return *this;
}

weak_entry_t(objc_object *newReferent, objc_object **newReferrer)
: referent(newReferent) // 构造方法,里面初始化了静态数组
{
inline_referrers[0] = newReferrer;
for (int i = 1; i < WEAK_INLINE_COUNT; i++) {
inline_referrers[i] = nil;
}
}
};

内部有定长数组inline_referrers[WEAK_INLINE_COUNT]和动态数组weak_referrer_t *referrers来存储弱引用对象的地址。通过out_of_line()这样的函数来判断该用哪种存储方式。

关联对象有什么应用,系统如何管理关联对象?其被释放的时候需要手动将所有的关联对象的指针置空么?

应用

  1. 添加公共属性,为分类动态的添加set get方法
  2. 为系统类添加方法,例如为UIView添加点击事件
  3. 使用KVO时关联对象作为观察者,避免自身观察自身

系统通过AssociationsManager 管理一个全局哈希表AssociationsHashMap,通过对象指针地址和传递的固定参数地址来获取关联对象。根据setter传入的参数协议,来管理对象的生命周期。

释放

对象释放时,会调用_object_remove_assocations方法自动释放我们的关联对象。

KVO的底层实现?如何取消系统默认的KVO并手动触发(给KVO的触发设定条件:改变的值符合某个条件时再触发KVO)?

底层实现

执行addObserver后runtime会创建一个这个类的子类,并将指针指向这个子类,这个子类的set方法会调用Fundation框架中的C语言函数_NSsetIntValueAndNotify(根据类型不同调用不同函数)。其中这个C语言函数就相当于调用willChangeValueForKey,接着调用父类的set方法,最后调用didChangeValueForKey。其实didChangeValueForKey中会调用observeValueForKeyPath

取消默认KVO增加判断条件

仅需要在对象中重写didChangeValueForKye方法,增加判断,当满足条件才调用[super didChangeValueForKey:key];

AutoreleasePool所使用的数据结构是什么?AutoreleasePoolPage结构体了解么?

AutoreleasePool所使用的数据结构__AtAutoreleasePool,是由若干个AutoreleasePoolPage以双向链表的形式存在的。

AutoreleasePoolPage

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class AutoreleasePoolPage 
{
# define EMPTY_POOL_PLACEHOLDER ((id*)1) // EMPTY_POOL_PLACEHOLDER:表示一个空自动释放池的占位符
# define POOL_BOUNDARY nil // POOL_BOUNDARY:哨兵对象
static pthread_key_t const key = AUTORELEASE_POOL_KEY;
static uint8_t const SCRIBBLE = 0xA3; // 用来标记已释放的对象
static size_t const SIZE = // 每个 Page 对象占用 4096 个字节内存
#if PROTECT_AUTORELEASEPOOL // PAGE_MAX_SIZE = 4096
PAGE_MAX_SIZE; // must be muliple of vm page size
#else
PAGE_MAX_SIZE; // size and alignment, power of 2
#endif
static size_t const COUNT = SIZE / sizeof(id); // Page 的个数

magic_t const magic; // 用来校验 Page 的结构是否完整
id *next; // 指向下一个可存放 autorelease 对象地址的位置,初始化指向 begin()
pthread_t const thread; // 指向当前线程
AutoreleasePoolPage * const parent; // 指向父结点,首结点的 parent 为 nil
AutoreleasePoolPage *child; // 指向子结点,尾结点的 child 为 nil
uint32_t const depth; // Page 的深度,从 0 开始递增
uint32_t hiwat;
......
}

每个AutoreleasePoolPage对象占有4096字节内存,其中56个字节用来存储它内部的成员变量,其余的用于存储autorelease对象的地址。

  1. 每当创建一个自动释放池时,会调用push()方法,将一个POOL_BOUNDARY入栈,并返回其存放的内存地址。
  2. 当往自动释放池添加autorelease对象时,将autorelease对象的内存地址入栈,它们前面至少有一个POOL_BOUNDARY
  3. 当销毁一个自动释放池时,会调用pop()方法,并传入一个POOL_BOUNDARY,会从自动释放池中最后一个对象开始释放,直到遇到这个POOL_BOUNDARY

讲一下对象,类对象,元类,跟元类结构体的组成以及他们是如何相关联的?为什么对象方法没有保存的对象结构体里,而是保存在类对象的结构体里?

对象的结构是objc_object,类对象和元类的结构是objc_class

对象的isa指针指向对应的类对象,类对象的isa指针指向元类。元类的isa指针都指向根元类, 其中根元类的isa指针指向自己。

类对象的superClass指针指向其父类,其中NSObject的superClass指针为nil,根元类的superClass指针指向NSObject类对象。

同个类的对象方法都是一样的,如果在每个对象中存放一份,则会产生浪费,而保存在类对象中,就能做到同个类的对象方法只存放一份。同样类方法则存放在元类中。

class_ro_tclass_rw_t 的区别?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
struct class_ro_t {
uint32_t flags;
uint32_t instanceStart;
uint32_t instanceSize;
uint32_t reserved;

const uint8_t * ivarLayout;

const char * name;
method_list_t * baseMethodList;
protocol_list_t * baseProtocols;
const ivar_list_t * ivars;

const uint8_t * weakIvarLayout;
property_list_t *baseProperties;
};
1
2
3
4
5
6
7
8
9
10
11
12
13
struct class_rw_t {
uint32_t flags;
uint32_t version;

const class_ro_t *ro;

method_array_t methods;
property_array_t properties;
protocol_array_t protocols;

Class firstSubclass;
Class nextSiblingClass;
};

每个类都对应一个class_ro_t和class_rw_t。在编译期间,确定class_ro_t的内容。在runtime的realizeClass方法运行时,生成class_rw_t,class_rw_t包括class_ro_t,并更新data部分。实际访问类的内容时,是访问class_rw_t中的内容。

iOS 中内省的几个方法?class方法和objc_getClass方法有什么区别?

1
2
3
4
-(BOOL) isKindOfClass:            		判断是否是这个类或者这个类的子类的实例
-(BOOL) isMemberOfClass: 判断是否是这个类的实例
-(BOOL) respondsToSelector: 判读实例是否有这样方法
+(BOOL) instancesRespondToSelector: 判断类是否有这个方法

objc_getClass(obj)方法返回的是isa指针。

[objc class]要两类情况

当objc为实例对象时,class是实例方法,返回的是objc对象的isa指针,也就是类对象。

当objc为类对象(包括元类,根类,以及根元类)时,class是类方法,返回的是它本身。

在运行时创建类的方法objc_allocateClassPair的方法名尾部为什么是pair(成对的意思)?

因为要创建类和元类,所以是成对的。

一个int变量被__block修饰与否的区别?

默认为值捕获,加__block后生成结构体,复制int的引用地址。

为什么在block外部使用__weak修饰的同时需要在内部使用__strong修饰?

首先weak是用于解决循环引用的问题的。同时因为weak的存在,可能会发生self在block执行的过程中被置为nil,这时就需要strong来将self保持住,避免发生意想不到的事。

RunLoop的作用是什么?它的内部工作机制了解么?(最好结合线程和内存管理来说)

一般来说线程在执行完任务后,就会退出。如果需要线程能够一直响应,则需要在内部添加类似循环的概念,类似EvenLoop,在没有处理消息的时候线程进行休眠,等消息来了立刻唤醒响应消息。

在iOS中这个循环就是RunLoop。主要的作用是:

  • 保持app的持续运行,在main方法中会给主线程创建一个RunLoop,保证线程不被销毁
  • AutoreleasePool,系统在主线程的RunLoop中注册了两个Observer。第一个Observer监听即将进入RunLoop,调用_objc_autoreleasePoolPush创建自动释放池。第二个Observer监听了两个事件,进入休眠之前会先释放自动释放池,然后创建一个自动释放池,在即将退出时,会释放自动释放池。
  • 检测卡顿,通过监听主线程的RunLoop的状态来判断是否会出现卡顿

哪些场景可以触发离屏渲染?(知道多少说多少)

离屏渲染:因为一些限制,无法直接将渲染结果写入帧缓冲区,而需要新开辟一个缓冲区来进行渲染。

  • 为contents设置了内容,无论是图片,绘制内容,有图像信息的子视图等,再加上圆角+裁剪
  • 采用了光栅化的 layer (layer.shouldRasterize)
  • 使用了 mask 的 layer (layer.mask)
  • 需要进行裁剪的 layer (layer.masksToBounds /view.clipsToBounds)
  • 设置了组透明度为 YES,并且透明度不为 1 的layer (layer.allowsGroupOpacity/ layer.opacity)
  • 使用了高斯模糊
  • 添加了投影的 layer (layer.shadow*)
  • 绘制了文字的 layer (UILabel, CATextLayer, Core Text 等)

了解编译的过程么?分为哪几个步骤?

  1. 预处理阶段
  2. 词法分析 输出Token流
  3. 语法分析 输出抽象树AST
  4. codegen生成IR中间码
  5. Optimize - 优化IR
  6. LLVM Bitcode - 生成字节码
  7. 生成汇编
  8. Link生成目标文件并执行

Xcode编译的过程

  1. 优化编译Cocoapods里面的所有依赖文件
  2. 编译信息写入辅助信息,创建编译后的文件架构
  3. 处理打包信息,例如development环境下处理xxxx.entitlements的打包信息
  4. 执行cocopods编译前脚本 checkPods Manifest.lock
  5. 编译包内所有m文件 (使用Compilec和Clang的几个主要命令)
  6. 链接需要的framework,例如AFNetworking.framework,Masonry.framework等信息
  7. 编译xib文件
  8. copy Xib文件,图片等资源文件放到结果目录
  9. 编译imageAsserts
  10. 处理infoplis
  11. 执行Cocoapods脚本
  12. copy标准库
  13. 创建.app文件和签名

App启动的过程

  1. 解析info.plist文件
  2. 创建沙盒
  3. 根据info.plist检查各权限状态
  4. 加载Mach-O文件读取dyld路径并运行dyld动态连接器(内核加载主程序,dyld只负责动态库的加载)
  5. dyld寻找合适的CPU运行环境
  6. 加载程序所需要的依赖库和我们的.h .m文件编译成的.o可执行文件,并对这些库进行链接
  7. 加载所有方法(runtime完成初始化)
  8. 加载C函数
  9. 加载category扩展(runtime对类结构进行初始化)
  10. 加载C++静态函数 加载OC的+load方法
  11. dyld返回main函数地址 执行main函数

静态链接了解么?静态库和动态库的区别?

静态链接和动态链接都是把程序分割成一个个独立的模块,但是静态链接是运行前就用ld链接器链接成一个完整的程序;动态链接是程序主模块被加载时候,对应的Mach-O文件里有dyld加载命令,通过这个dyld然后去找依赖的dylib(Mach-O有动态链接库加载命令),把dylib加载到内存(如果对应的dylib不在内存),然后将程序中所有未决议的符号绑定到相应的 dylib中,并进行重定位工作。

静态库:完整复制进可执行的二进制文件中

动态库:在程序冷启动时才会被链接到手机内存或App内存中

内存的几大区域,各自的职能分别是什么?

内存主要分为栈区、堆区、全局区、文字常量区、代码区等五大区域。

  1. 栈区(stack)由编译器自动分配并释放,存放的是函数的参数值,局部变量等,方法调用的实参也是保存在栈区的。栈是系统数据结构,对应线程/进程是唯一的。主要存放一些基本类型的变量和对象引用类型。

  2. 堆区(heap)由程序员分配和释放,如果程序员不释放,可能会出现内存泄露,程序结束的时候,可能会由操作系统回收。堆空间的分配总是动态的,不同堆分配的内存无法互相操作。虽然程序结束的时候所有的数据空间都会被释放回系统,但是精确的申请内存,释放内存匹配是良好程序的基本要素。主要存放用new构造的对象和数组。

  3. 全局区(静态区) (static) 全局变量和静态变量的存储是放在一起的,初始化的全局变量和静态变量存放在一块区域,未初始化的全局变量和静态变量在相邻的另一块区域,程序结束后有系统释放。

  4. 文字常量区 存放常量字符串,程序结束后由系统释放;

  5. 代码区 存放函数的二进制代码

static和const有什么区别?

const

  • 被const关键字修饰的实例变量,在初始化之后,其值就不能改变了
  • 队指针来说,可以指定指针本身为const,也可以指定指针所指的数据为const,或者二者同时指定为const;
  • 对于类的成员函数,若指定其为const类型, 则表明他是一个函数,不能修改类的成员变量

static

  • 在函数体内定义的static他的作用域为该函数体,该变量在内存中只被分配一次,因此,其值在下次调用时仍维持上次的值不变
  • 在模块内的static全局变量可以被模块内所用函数访问,但是不能被模块外的其他函数访问
  • .在模块内的staic全局变量可以被这一模块内的其他函数调用,这个函数的使用范围被限制在这个模块内;
  • 在类中的static成员变量属于整个类所拥有,对类的所有对象只有一份拷贝,也就是说只要是该类的对象,那么该对象的中被static修饰的实例变量都指向同一块地址

了解内联函数么?

1
2
3
static inline CGFloat CGFloatFromPixel(CGFloat value) {
return value / YYScreenScale();
}

用于取代宏,避免了普通函数汇编时必须调用call的缺点,取消了函数的参数压栈,避免了宏的缺点,需要预编译。

什么时候会出现死锁?如何避免?

发生死锁的四个必要条件:

  • 互斥条件:一个资源每次只能被一个线程使用。
  • 请求与保持条件:一个线程因请求资源而阻塞时,对已获得的资源保持不放。
  • 不剥夺条件:线程已获得的资源,在未使用完之前,不能强行剥夺。
  • 循环等待条件:若干线程之间形成一种头尾相接的循环等待资源关系。

说一说你对线程安全的理解?

多线程操作共享数据不会出现想不到的结果就是线程安全的。

列举你知道的线程同步策略?

线程同步的策略有:原子操作,信号量,GCD串行队列,锁。

有哪几种锁?各自的原理?它们之间的区别是什么?最好可以结合使用场景来说

OSSpinLock - 自旋锁

  • 自旋锁是一种特殊互斥锁,当一个线程需要获取自旋锁时,如果该锁已经被其他线程占用,那么会一直去请求锁,进入 忙等(busy-waiting) 状态,所以会一直占用 CPU

  • 由于自旋锁在等待锁的时候线程一直处于忙等状态,而不用进入睡眠,所以不用进行上下文切换,自旋锁的效率远高于互斥锁

自旋锁适用于

  • 预计线程等待锁的时间很短
  • 临界区经常访问,但竞争情况很少发生
  • 自旋锁不安全,会出现优先级反转问题:如果一个低优先级的线程获得锁并访问共享资源,这时一个高优先级的线程也尝试获得这个锁,它会处于 忙等 状态从而占用大量 CPU 时间片。此时低优先级线程无法与高优先级线程争夺 CPU 时间片,从而导致完成任务而无法释放锁
  • 在 iOS 10 及以上被废弃
1
2
3
4
5
6
7
8
9
10
11
#import <libkern/OSAtomic.h>

OSSpinLock lock = OS_SPINLOCK_INIT;

// 加锁
OSSpinLockLock(&lock);

// 临界区...

// 解锁
OSSpinLockUnlock(&lock);

os_unfair_lock

  • os_unfair_lock 用于取代不安全的 OSSpinLock ,iOS 10 开始支持,当一条线程等待锁的时候会进入睡眠,不再消耗 CPU 时间,当其他线程解锁以后,操作系统会激活线程
  • os_unfair_lock 有单一的拥有者
  • 这是一种不公平锁。在公平锁中,多个线程同时竞争这个锁的时候, 会考虑公平性尽可能的让不同的线程获得锁,这样会频繁进行上下文切换,牺牲性能。而在不公平锁中,系统为了减少上下文切换,当前拥有锁的线程有可能会再次获得锁,但这样做可能会让其他线程等待更长时间,造成饥饿。
1
2
3
4
5
6
7
8
9
10
11
12
#import <os/lock.h>

os_unfair_lock lock = OS_UNFAIR_LOCK_INIT;


// 加锁
os_unfair_lock_lock(&lock);

// 临界区...

// 解锁
os_unfair_lock_unlock(&lock);

互斥锁

  • 互斥锁是可以看作是一种特殊的信号量,当一条线程等待锁的时候会进入睡眠状态
  • 互斥锁阻塞的过程分两个阶段,第一阶段是会先空转,可以理解成跑一个 while 循环,不断地去申请加锁,在空转一定时间之后,线程会进入睡眠状态,让出时间片,此时线程就不占用 CPU 时间片,等锁可用的时候,这个线程会立即被唤醒

pthread_mutex

pthread 表示 POSIX thread,是 POSIX 标准的 unix 多线程库,定义了一组跨平台的线程相关的API。pthread_mutex 是一种用 C 语言实现的互斥锁,有单一的拥有者。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
#import <pthread.h>

// 静态初始化
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;


// 动态初始化
// 初始化属性
pthread_mutexattr_t attr;
pthread_mutexattr_init(&attr);
pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_DEFAULT);
// 初始化锁
pthread_mutex_t mutex;
pthread_mutex_init(mutex, &attr);
// 销毁属性
pthread_mutexattr_destroy(&attr);


// 加锁
pthread_mutex_lock(&mutex);

// 临界区...

// 解锁
pthread_mutex_unlock(&mutex);

// 销毁锁
pthread_mutex_destroy(&_mutex);

NSLock

  • NSLock 是以 Objective-C 对象的形式对 pthread_mutex 的封装,属性为 PTHREAD_MUTEX_ERRORCHECK,它会损失一定性能换来错误提示
  • NSLockpthread_mutex 略慢的原因在于它需要经过方法调用,同时由于缓存的存在,多次方法调用不会对性能产生太大的影响
  • NSLock 有单一的拥有者
1
2
3
4
5
6
7
8
9
NSLock *lock = [[NSLock alloc] init];

// 加锁
[lock lock];

// 临界区...

// 解锁
[lock unlock];

递归锁

递归锁是一种特殊互斥锁。递归锁允许单个线程在释放之前多次获取锁,其他线程保持睡眠状态,直到锁的所有者释放锁的次数与获取它的次数相同。递归锁主要在递归迭代中使用,但也可能在多个方法需要单独获取锁的情况下使用。

pthread_mutex(Recursive)

pthread_mutex 支持递归锁,只要把 attr 的类型改成 PTHREAD_MUTEX_RECURSIVE 即可,它有单一的拥有者。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
#import <pthread.h>

// 初始化属性
pthread_mutexattr_t attr;
pthread_mutexattr_init(&attr);
pthread_mutexattr_settype(&attr, PTHREAD_MUTEX_RECURSIVE);
// 初始化锁
pthread_mutex_t mutex;
pthread_mutex_init(mutex, &attr);
// 销毁属性
pthread_mutexattr_destroy(&attr);


// 加锁
pthread_mutex_lock(&_mutex);


// 临界区...
// 在同一个线程中可以多次获取锁

// 解锁
pthread_mutex_unlock(&_mutex);


// 销毁锁
pthread_mutex_destroy(&_mutex);

NSRecursiveLock

NSRecursiveLock 是以 Objective-C 对象的形式对 pthread_mutex(Recursive) 的封装,它有单一的拥有者。

1
2
3
4
5
6
7
8
9
10
NSRecursiveLock *lock = [[NSRecursiveLock alloc] init];

// 加锁
[lock lock];

// 临界区...
// 在同一个线程中可以多次获取锁

// 解锁
[lock unlock];

@synchronized

  • @synchronized 是对 pthread_mutex(Recursive) 的封装,所以它支持递归加锁
  • 需要传入一个 Objective-C 对象,可以理解为把这个对象当做锁来使用
  • 实际上它是用 objc_sync_enter(id obj)objc_sync_exit(id obj) 来进行加锁和解锁
  • 底层实现:在底层存在一个全局用来存放锁的哈希表(可以理解为锁池),对传入的对象地址的哈希值作为key,去查找对应的递归锁
  • @synchronized 额外还会设置异常处理机制,性能消耗较大
  • @synchronized 有单一的拥有者
1
2
3
@synchronized(lock) {
// 临界区...
}

条件锁

条件锁是一种特殊互斥锁,需要条件变量(condition variable) 来配合。条件变量有点像信号量,提供了线程阻塞与信号机制,因此可以用来阻塞某个线程,并等待某个数据就绪,随后唤醒线程。条件锁是为了解决 生产者-消费者模型

pthread_mutex – 条件锁

pthread_mutex 配合 pthread_cond_t,可以实现条件锁,其中 pthread_cond_t 没有拥有者。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
#import <pthread.h>

// 初始化锁
pthread_mutex_t mutex;
pthread_mutex_init(&mutex, &NULL);
// 销毁属性
pthread_mutexattr_destroy(&attr);

// 初始化条件变量
pthread_cond_t cond;
pthread_cond_init(&cond, NULL);

// 消费者
- (void)remove {
// 加锁
pthread_mutex_lock(&mutex);

// 先判断某个条件
if (self.data.count == 0) {
// 如果不满足条件,则等待,具体是释放锁,用条件变量来阻塞当前线程
// 当条件满足的时候,条件变量唤醒线程,再用原来的锁加锁
pthread_cond_wait(&cond, &mutex);
}

[self.data removeLastObject];


// 解锁
pthread_mutex_unlock(&mutex);
}


// 生产者
- (void)add
{
// 加锁
pthread_mutex_lock(&mutex);


[self.data addObject:@"Test"];

// 信号
// 条件变量唤醒阻塞的线程
pthread_cond_signal(&cond);
// 广播
// pthread_cond_broadcast(&cond);

// 解锁
pthread_mutex_unlock(&mutex);
}


// 销毁
pthread_mutex_destroy(&mutex);
pthread_cond_destroy(&cond);

NSCondition

NSCondition 是以 Objective-C 对象的形式对 pthread_mutexpthread_cond_t 进行了封装,NSCondition 没有拥有者。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
NSCondition *condition = [[NSCondition alloc] init];

// 消费者
- (void)remove
{
[condition lock];


if (self.data.count == 0) {
// 如果不满足条件,则等待,具体是释放锁,用条件变量来阻塞当前线程
// 当条件满足的时候,条件变量唤醒线程,再用原来的锁加锁
[condition wait];
}

[self.data removeLastObject];

[condition unlock];
}


// 生产者
- (void)add
{
[condition lock];


[self.data addObject:@"Test"];

// 信号
// 条件变量唤醒阻塞的线程
[condition signal];


[condition unlock];
}

NSConditionLock

NSConditionLock 是对 NSCondition 的进一步封装,可以设置条件变量的值。通过改变条件变量的值,可以使任务之间产生依赖关系,达到使任务按照一定的顺序执行,它有单一的拥有者(不确定)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
// 初始化设置条件变量的为1,如果不设置则默认为0
NSConditionLock *lock = [[NSConditionLock alloc] initWithCondition:1];


// 消费者
- (void)remove
{
// 当条件变量为2的时候加锁,否则等待
[lock lockWhenCondition:2];

[self.data removeLastObject];

// 直接解锁
[lock unlock];
}


// 生产者
- (void)add
{
// 直接加锁
[lock lock];


[self.data addObject:@"Test"];


// 解锁并让条件变量为2
[lock unlockWithCondition:2];
}

读写锁

读写锁是一种特殊互斥锁,提供”多读单写”的功能,多个线程可以同时对共享资源进行读取,但是同一时间只能有一条线程对共享资源进行写入。

pthread_rwlock

pthread_rwlock 有多个拥有者。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
#import <pthread.h>

// 初始化
pthread_rwlock_t lock = PTHREAD_RWLOCK_INITIALIZER;


// 读操作
- (void)read {
pthread_rwlock_rdlock(&lock);

// 临界区...

pthread_rwlock_unlock(&lock);
}

// 写操作
- (void)write
{
pthread_rwlock_wrlock(&lock);

// 临界区...

pthread_rwlock_unlock(&lock);
}

// 销毁
- (void)dealloc
{
pthread_rwlock_destroy(&lock);
}

GCD 的 Barrier函数

  • GCD 的 Barrier 函数也可以实现”多读单写”的功能

  • Barrier 函数的作用是:等其他任务执行完毕,才会执行任务自己的任务;会执行完毕自己的任务,才会继续执行其他任务

  • 这个函数传入的并发队列必须是自己通过 dispatch_queue_cretate 创建的,如果传入的是一个串行或是一个全局的并发队列,那这个函数便等同于 dispatch_async 函数的效果

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
dispatch_queue_t queue = dispatch_queue_create("rw_queue", DISPATCH_QUEUE_CONCURRENT);


dispatch_async(queue, ^{
// 读
});

dispatch_async(queue, ^{
// 读
});


dispatch_barrier_async(queue, ^{
// 写
});

dispatch_async(queue, ^{
// 读
});

总结:

  • OSSpinLockos_unfair_lock 性能很高,但是一个是已经废弃,一个是低级锁,苹果不建议使用低级锁
  • dispatch_semaphorepthread_mutex 也具有不错的性能,NSLockpthread_mutex 的封装,性能上接近
  • 个人建议在 Objective-C 中直接使用面向对象的 NSLock,而在 Swif t中使用 GCD 串行队列

除了单例,观察者设计模式以外,还知道哪些设计模式?分别介绍一下

工厂模式:定义一个用于创建对象的接口,让子类决定实例化哪一个类。工厂方法使一个类的实例化延迟到子类。

装饰器模式:不改变原有对象的前提下,动态地给一个对象增加一些额外的功能。

代理模式:为某个对象提供一个代理,并由这个代理对象控制对原对象的访问。

命令模式:将一个请求封装为一个对象,从而让我们可用不同的请求对客户进行参数化,或者说如何将“行为请求者”与“行为实现者”解耦?将一组行为抽象为对象,实现二者之间的松耦合。

享元模式:主要用于减少同一类对象的大量创建,以减少内存占用,提高项目流畅度。在享元模式中有两个重要的概念,即内部状态和外部状态:

  • 内部状态:在享元对象内部不随外界环境改变而改变的共享部分
  • 外部状态:随着环境的改变而改变,不能够共享的状态就是外部状态

最喜欢哪个设计模式?为什么?

iOS中最常见用到的设计模式便是代理模式。但要说最喜欢的还是装饰器模式。整个模式非常灵活,它将各种能力抽象出来,当某个对象需要某种能力时,只需要简单集成就可以,非常清晰。

iOS SDK 里面有哪些设计模式的实践?

代理模式:delegate

工厂模式:NSNumber

1
2
3
4
+ (NSNumber *)numberWithInt:(int)value;
+ (NSNumber *)numberWithBool:(BOOL)value;
+ (NSNumber *)numberWithFloat:(float)value;
+ (NSNumber *)numberWithDouble:(double)value;

装饰器模式:Catagory

观察者模式:KVO

单例模式:dispatch_once

享元模式:UITableViewCell 重用

设计模式是为了解决什么问题的?

设计模式(Design pattern)是一套被反复使用、多数人知晓的、经过分类编目的、代码百设计经验的总结。使用设计模式是为了可重用代码、让代码更容易被度他人理解、保证代码可靠性。
设计模式最主要解决的问题是通过封装和隔离变化点来处理软件的各种变化问题。
隔离变化的好处在于,将问系统中经常变化的部分和稳定的部分隔离,有助于增加复用性,并降低系统耦合度。很多设计模答式的意图中都明显地版指出了其对问题的解决方案,学习设计模式的要点是发现其解决方案中封装的变化点。

MVC和MVVM的区别?MVVM和MVP的区别?

MVC主要的贡献在于对软件整体进行分层。主要分为三层,Model,View,Controller。View直接通过读取Model进行显示。

MVP由MVC演化过来,相通的地方Controller/Presenter负责逻辑的处理,Model提供数据,View负责显示。主要的区别在于View不直接使用Model,它们之间通信是通过Presenter进行的。

MVVM将View的状态和行为抽象化,

面向对象的几个设计原则了解么?最好可以结合场景来说。

开闭原则(Open Close Principle):个软件实体如类、模块和函数应该对扩展开放,对修改关闭。

  • 用抽象构建框架,用实现扩展细节。
  • 不以改动原有类的方式来实现新需求,而是应该以实现事先抽象出来的接口(或具体类继承抽象类)的方式来实现。

单一职责原则(Single Responsibility Principle):一个类只允许有一个职责,即只有一个导致该类变更的原因。

依赖倒置原则(Dependency Inversion Principle)

  • 依赖抽象,而不是依赖实现。
  • 抽象不应该依赖细节;细节应该依赖抽象。
  • 高层模块不能依赖低层模块,二者都应该依赖抽象。

接口分离原则(Interface Segregation Principle):多个特定的客户端接口要好于一个通用性的总接口。

  • 客户端不应该依赖它不需要实现的接口。
  • 不建立庞大臃肿的接口,应尽量细化接口,接口中的方法应该尽量少。

迪米特法则(Law of Demeter):一个对象应该对尽可能少的对象有接触,也就是只接触那些真正需要接触的对象。

  • 一个对象应该对尽可能少的对象有接触,也就是只接触那些真正需要接触的对象。

里氏替换原则(Liskov Substitution Principle):所有引用基类的地方必须能透明地使用其子类的对象,也就是说子类对象可以替换其父类对象,而程序执行效果不变。

在继承体系中,子类中可以增加自己特有的方法,也可以实现父类的抽象方法,但是不能重写父类的非抽象方法,否则该继承关系就不是一个正确的继承关系。

可以说几个重构的技巧么?你觉得重构适合什么时候来做?

你觉得框架和设计模式的区别是什么?

看过哪些第三方框架的源码,它们是怎么设计的?设计好的地方在哪里,不好的地方在哪里,如何改进?

链表和数组的区别是什么?插入和查询的时间复杂度分别是多少?

链表

  • 可以在内存的任何地方,不要求连续
  • 每个数据中保存了下一个数据的内存地址
  • 插入快 O(1) 查询慢O(n) 删除O(1)

数组

  • 是一块连续的内存
  • 需要预留空间,使用前需要申请空间
  • 插入慢O(n) 查询快O(1) 删除O(1)

哈希表是如何实现的?如何解决地址冲突?

哈希表(hash table,也叫散列表),是根据键(key)直接访问访问在内存储存位置的数据结构。 哈希表本质是一个数组,数组中的每一个元素成为一个箱子,箱子中存放的是键值对。根据下标index从数组中取value。关键是如何获取index,这就需要一个固定的函数(哈希函数),将key转换成index。不论哈希函数设计的如何完美,都可能出现不同的key经过hash处理后得到相同的hash值,这时候就需要处理哈希冲突。

拉链法

简单来说就是 数组 + 链表 。将键通过hash函数映射为大小为M的数组的下标索引,数组的每一个元素指向一个链表,链表中的每一个结点存储着hash出来的索引值为结点下标的键值对。

  1. 拉链法处理冲突简单,且无堆积现象,即非同义词决不会发生冲突,因此平均查找长度较短;

  2. 由于拉链法中各链表上的结点空间是动态申请的,故它更适合于造表前无法确定表长的情况;

  3. 开放定址线性探测发为减少冲突,要求装填因子α较小,故当结点规模较大时会浪费很多空间。而拉链法中可取α≥1,且结点较大时,拉链法中增加的指针域可忽略不计,因此节省空间;

  4. 在用拉链法构造的散列表中,删除结点的操作易于实现。只要简单地删去链表上相应的结点即可。而对开放定址线性探测发构造的散列表,删除结点不能简单地将被删结 点的空间置为空,否则将截断在它之后填人散列表的同义词结点的查找路径。这是因为各种开放定址线性探测发中,空地址单元(即开放地址)都是查找失败的条件。因此在用开放定址线性探测发处理冲突的散列表上执行删除操作,只能在被删结点上做删除标记,而不能真正删除结点。

  5. 指针需要额外的空间,故当结点规模较小时,开放定址线性探测发较为节省空间,而若将节省的指针空间用来扩大散列表的规模,可使装填因子变小,这又减少了开放定址线性探测发中的冲突,从而提高平均查找速度。

开放定址线性探测法

使用两个大小为N的数组(一个存放keys,另一个存放values)。使用数组中的空位解决碰撞,当碰撞发生时(即一个键的hash值对应数组的下标被另外一个键占用)直接将下标索引加一(index += 1)。

  1. 容易产生堆积问题;

  2. 不适于大规模的数据存储;

  3. 散列函数的设计对冲突会有很大的影响;
  4. 插入时可能会出现多次冲突的现象,删除的元素是多个冲突元素中的一个,需要对后面的元素作处理,实现较复杂;
  5. 结点规模很大时会浪费很多空间;
CATALOG
  1. 1. 分类和扩展有什么区别?可以分别用来做什么?分类有哪些局限性?分类的结构体里面有哪些成员?
  2. 2. 讲一下atomic的实现机制;为什么不能保证绝对的线程安全(最好可以结合场景来说)?
  3. 3. 被weak修饰的对象在被释放的时候会发生什么?是如何实现的?知道sideTable么?里面的结构可以画出来么?
  4. 4. 关联对象有什么应用,系统如何管理关联对象?其被释放的时候需要手动将所有的关联对象的指针置空么?
  5. 5. KVO的底层实现?如何取消系统默认的KVO并手动触发(给KVO的触发设定条件:改变的值符合某个条件时再触发KVO)?
  6. 6. AutoreleasePool所使用的数据结构是什么?AutoreleasePoolPage结构体了解么?
  7. 7. 讲一下对象,类对象,元类,跟元类结构体的组成以及他们是如何相关联的?为什么对象方法没有保存的对象结构体里,而是保存在类对象的结构体里?
  8. 8. class_ro_t 和 class_rw_t 的区别?
  9. 9. iOS 中内省的几个方法?class方法和objc_getClass方法有什么区别?
  10. 10. 在运行时创建类的方法objc_allocateClassPair的方法名尾部为什么是pair(成对的意思)?
  11. 11. 一个int变量被__block修饰与否的区别?
  12. 12. 为什么在block外部使用__weak修饰的同时需要在内部使用__strong修饰?
  13. 13. RunLoop的作用是什么?它的内部工作机制了解么?(最好结合线程和内存管理来说)
  14. 14. 哪些场景可以触发离屏渲染?(知道多少说多少)
  15. 15. 了解编译的过程么?分为哪几个步骤?
  16. 16. App启动的过程
  17. 17. 静态链接了解么?静态库和动态库的区别?
  18. 18. 内存的几大区域,各自的职能分别是什么?
  19. 19. static和const有什么区别?
  20. 20. 了解内联函数么?
  21. 21. 什么时候会出现死锁?如何避免?
  22. 22. 说一说你对线程安全的理解?
  23. 23. 列举你知道的线程同步策略?
  24. 24. 有哪几种锁?各自的原理?它们之间的区别是什么?最好可以结合使用场景来说
    1. 24.1. 互斥锁
    2. 24.2. 递归锁
    3. 24.3. 条件锁
    4. 24.4. 读写锁
    5. 24.5. 除了单例,观察者设计模式以外,还知道哪些设计模式?分别介绍一下
  25. 25. 最喜欢哪个设计模式?为什么?
  26. 26. iOS SDK 里面有哪些设计模式的实践?
  27. 27. 设计模式是为了解决什么问题的?
  28. 28. MVC和MVVM的区别?MVVM和MVP的区别?
  29. 29. 面向对象的几个设计原则了解么?最好可以结合场景来说。
  30. 30. 可以说几个重构的技巧么?你觉得重构适合什么时候来做?
  31. 31. 你觉得框架和设计模式的区别是什么?
  32. 32. 看过哪些第三方框架的源码,它们是怎么设计的?设计好的地方在哪里,不好的地方在哪里,如何改进?
  33. 33. 链表和数组的区别是什么?插入和查询的时间复杂度分别是多少?
  34. 34. 哈希表是如何实现的?如何解决地址冲突?