(答案不唯一,仅供参考,文章最后有福利)
一. iOS面试题---UI相关:事件传递,图像显示,性能优化,离屏渲染
- UIView与CALayer
- 事件传递与视图响应链
- 图像显示原理
- UI卡顿掉帧原因
- 滑动优化方案
- UI绘制原理
- 离屏渲染
一、UIView与CALayer
<单一职责原则>
UIView为CALayer提供内容,以及负责处理触摸等事件,参与响应链
CALayer负责显示内容contents
二、事件传递与视图响应链 :
- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event;
- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event;
如果事件一直传递到UIAppliction还是没处理,那就会忽略掉
三、图像显示原理
1.CPU:输出位图
2.GPU :图层渲染,纹理合成
3.把结果放到帧缓冲区(frame buffer)中
4.再由视频控制器根据vsync信号在指定时间之前去提取帧缓冲区的屏幕显示内容
5.显示到屏幕上
CPU工作
1.Layout: UI布局,文本计算
2.Display: 绘制
3.Prepare: 图片解码
4.Commit:提交位图
GPU渲染管线(OpenGL)
顶点着色,图元装配,光栅化,片段着色,片段处理
四、UI卡顿掉帧原因
iOS设备的硬件时钟会发出Vsync(垂直同步信号),然后App的CPU会去计算屏幕要显示的内容,之后将计算好的内容提交到GPU去渲染。随后,GPU将渲染结果提交到帧缓冲区,等到下一个VSync到来时将缓冲区的帧显示到屏幕上。也就是说,一帧的显示是由CPU和GPU共同决定的。
一般来说,页面滑动流畅是60fps,也就是1s有60帧更新,即每隔16.7ms就要产生一帧画面,而如果CPU和GPU加起来的处理时间超过了16.7ms,就会造成掉帧甚至卡顿。
五、滑动优化方案
CPU:把以下操作放在子线程中
1.对象创建、调整、销毁
2.预排版(布局计算、文本计算、缓存高度等等)
3.预渲染(文本等异步绘制,图片解码等)
GPU:
纹理渲染,视图混合
一般遇到性能问题时,考虑以下问题:
是否受到CPU或者GPU的限制?
是否有不必要的CPU渲染?
是否有太多的离屏渲染操作?
是否有太多的图层混合操作?
是否有奇怪的图片格式或者尺寸?
是否涉及到昂贵的view或者效果?
view的层次结构是否合理?
六、UI绘制原理
异步绘制:
[self.layer.delegate displayLayer: ]
代理负责生成对应的bitmap
设置该bitmap作为该layer.contents属性的值
七、离屏渲染
On-Screen Rendering:当前屏幕渲染,指的是GPU的渲染操作是在当前用于显示的屏幕缓冲区中进行
Off-Screen Rendering:离屏渲染,分为CPU离屏渲染和GPU离屏渲染两种形式。GPU离屏渲染指的是GPU在当前屏幕缓冲区外新开辟一个缓冲区进行渲染操作
应当尽量避免的则是GPU离屏渲染
GPU离屏渲染何时会触发呢?
圆角(当和maskToBounds一起使用时)、图层蒙版、阴影,设置
layer.shouldRasterize = YES
为什么要避免GPU离屏渲染?
GPU需要做额外的渲染操作。通常GPU在做渲染的时候是很快的,但是涉及到offscreen-render的时候情况就可能有些不同,因为需要额外开辟一个新的缓冲区进行渲染,然后绘制到当前屏幕的过程需要做onscreen跟offscreen上下文之间的切换,这个过程的消耗会比较昂贵,涉及到OpenGL的pipeline跟barrier,而且offscreen-render在每一帧都会涉及到,因此处理不当肯定会对性能产生一定的影响。另外由于离屏渲染会增加GPU的工作量,可能会导致CPU+GPU的处理时间超出16.7ms,导致掉帧卡顿。所以可以的话应尽量减少offscreen-render的图层
二.Objective_C语言特性:分类、扩展、代理、通知、KVO、KVC、属性
- 分类
- 扩展
- 代理(Delegate)
- 通知(NSNotification)
- KVO (Key-value observing)
- KVC (Key-value coding)
- 属性关键字
一、分类
-
1.分类的作用?
声明私有方法,分解体积大的类文件,把framework的私有方法公开 -
2.分类的特点
运行时决议,可以为系统类添加分类 。
说得详细些,在运行时时期,将 Category 中的实例方法列表、协议列表、属性列表添加到主类中后(所以Category中的方法在方法列表中的位置是在主类的同名方法之前的),然后会递归调用所有类的 load 方法,这一切都是在main函数之前执行的。 -
3.分类可以添加哪些内容?
实例方法,类方法,协议,属性(添加getter和setter方法,并没有实例变量,添加实例变量需要用关联对象) -
4.如果工程里有两个分类A和B,两个分类中有一个同名的方法,哪个方法最终生效?
取决于分类的编译顺序,最后编译的那个分类的同名方法最终生效,而之前的都会被覆盖掉(这里并不是真正的覆盖,因为其余方法仍然存在,只是访问不到,因为在动态添加类的方法的时候是倒序遍历方法列表的,而最后编译的分类的方法会放在方法列表前面,访问的时候就会先被访问到,同理如果声明了一个和原类方法同名的方法,也会覆盖掉原类的方法)。 -
5.如果声明了两个同名的分类会怎样?
会报错,所以第三方的分类,一般都带有命名前缀 -
6.分类能添加成员变量吗?
不能。只能通过关联对象(objc_setAssociatedObject)来模拟实现成员变量,但其实质是关联内容,所有对象的关联内容都放在同一个全局容器哈希表中:AssociationsHashMap,由AssociationsManager统一管理。
二、扩展
-
1.一般用扩展做什么?
声明私有属性,声明方法(没什么意义),声明私有成员变量 -
2.扩展的特点
编译时决议,只能以声明的形式存在,多数情况下寄生在宿主类的.m中,不能为系统类添加扩展。
三、代理(Delegate)
代理是一种设计模式,以@protocol形式体现,一般是一对一传递。
一般以weak关键词以规避循环引用。
四、通知(NSNotification)
使用观察者模式来实现的用于跨层传递信息的机制。传递方式是一对多的。
-
如果实现通知机制?
五、KVO (Key-value observing)
KVO是观察者模式的另一实现。
使用了isa混写(isa-swizzling)来实现KVO
使用setter方法改变值KVO会生效,使用setValue:forKey即KVC改变值KVO也会生效,因为KVC会去调用setter方法
- (void)setValue:(id)value
{
[self willChangeValueForKey:@"key"];
[super setValue:value];
[self didChangeValueForKey:@"key"];
}
-
那么通过直接赋值成员变量会触发KVO吗?
不会,因为不会调用setter方法,需要加上
willChangeValueForKey和didChangeValueForKey方法来手动触发才行
六、KVC(Key-value coding)
-(id)valueForKey:(NSString *)key;
-(void)setValue:(id)value forKey:(NSString *)key;
KVC就是指iOS的开发中,可以允许开发者通过Key名直接访问对象的属性,或者给对象的属性赋值。而不需要调用明确的存取方法。这样就可以在运行时动态地访问和修改对象的属性。而不是在编译时确定,这也是iOS开发中的黑魔法之一。很多高级的iOS开发技巧都是基于KVC实现的
当调用setValue:属性值 forKey:@”name“的代码时,,底层的执行机制如下:
- 程序优先调用set<Key>:属性值方法,代码通过setter方法完成设置。注意,这里的<key>是指成员变量名,首字母大小写要符合KVC的命名规则,下同
- 如果没有找到setName:方法,KVC机制会检查+ (BOOL)accessInstanceVariablesDirectly方法有没有返回YES,默认该方法会返回YES,如果你重写了该方法让其返回NO的话,那么在这一步KVC会执行setValue:forUndefinedKey:方法,不过一般开发者不会这么做。所以KVC机制会搜索该类里面有没有名为<key>的成员变量,无论该变量是在类接口处定义,还是在类实现处定义,也无论用了什么样的访问修饰符,只在存在以<key>命名的变量,KVC都可以对该成员变量赋值。
- 如果该类即没有set<key>:方法,也没有_<key>成员变量,KVC机制会搜索_is<Key>的成员变量。
- 和上面一样,如果该类即没有set<Key>:方法,也没有_<key>和_is<Key>成员变量,KVC机制再会继续搜索<key>和is<Key>的成员变量。再给它们赋值。
- 如果上面列出的方法或者成员变量都不存在,系统将会执行该对象的setValue:forUndefinedKey:方法,默认是抛出异常。
即如果没有找到Set<Key>方法的话,会按照_key,_iskey,key,iskey的顺序搜索成员并进行赋值操作。
如果开发者想让这个类禁用KVC,那么重写+ (BOOL)accessInstanceVariablesDirectly方法让其返回NO即可,这样的话如果KVC没有找到set<Key>:属性名时,会直接用setValue:forUndefinedKey:方法。
当调用valueForKey:@”name“的代码时,KVC对key的搜索方式不同于setValue:属性值 forKey:@”name“,其搜索方式如下:
- 首先按get<Key>,<key>,is<Key>的顺序方法查找getter方法,找到的话会直接调用。如果是BOOL或者Int等值类型, 会将其包装成一个NSNumber对象。
- 如果上面的getter没有找到,KVC则会查找countOf<Key>,objectIn<Key>AtIndex或<Key>AtIndexes格式的方法。如果countOf<Key>方法和另外两个方法中的一个被找到,那么就会返回一个可以响应NSArray所有方法的代理集合(它是NSKeyValueArray,是NSArray的子类),调用这个代理集合的方法,或者说给这个代理集合发送属于NSArray的方法,就会以countOf<Key>,objectIn<Key>AtIndex或<Key>AtIndexes这几个方法组合的形式调用。还有一个可选的get<Key>:range:方法。所以你想重新定义KVC的一些功能,你可以添加这些方法,需要注意的是你的方法名要符合KVC的标准命名方法,包括方法签名。
- 如果上面的方法没有找到,那么会同时查找countOf<Key>,enumeratorOf<Key>,memberOf<Key>格式的方法。如果这三个方法都找到,那么就返回一个可以响应NSSet所的方法的代理集合,和上面一样,给这个代理集合发NSSet的消息,就会以countOf<Key>,enumeratorOf<Key>,memberOf<Key>组合的形式调用。
- 如果还没有找到,再检查类方法+ (BOOL)accessInstanceVariablesDirectly,如果返回YES(默认行为),那么和先前的设值一样,会按_<key>,_is<Key>,<key>,is<Key>的顺序搜索成员变量名,这里不推荐这么做,因为这样直接访问实例变量破坏了封装性,使代码更脆弱。如果重写了类方法+ (BOOL)accessInstanceVariablesDirectly返回NO的话,那么会直接调用valueForUndefinedKey:方法,默认是抛出异常。
七、属性关键字
1.读写权限:readonly,readwrite(默认)
2.原子性: atomic(默认),nonatomic。atomic读写线程安全,但效率低,而且不是绝对的安全,比如如果修饰的是数组,那么对数组的读写是安全的,但如果是操作数组进行添加移除其中对象的还,就不保证安全了。
3.引用计数:
- retain/strong
- assign:修饰基本数据类型,修饰对象类型时,不改变其引用计数,会产生悬垂指针,修饰的对象在被释放后,assign指针仍然指向原对象内存地址,如果使用assign指针继续访问原对象的话,就可能会导致内存泄漏或程序异常
- weak:不改变被修饰对象的引用计数,所指对象在被释放后,weak指针会自动置为nil
- copy:分为深拷贝和浅拷贝
浅拷贝:对内存地址的复制,让目标对象指针和原对象指向同一片内存空间会增加引用计数
深拷贝:对对象内容的复制,开辟新的内存空间
可变对象的copy和mutableCopy都是深拷贝
不可变对象的copy是浅拷贝,mutableCopy是深拷贝
copy方法返回的都是不可变对象
-
@property (nonatomic, copy) NSMutableArray * array;这样写有什么影响?
因为copy方法返回的都是不可变对象,所以array对象实际上是不可变的,如果对其进行可变操作如添加移除对象,则会造成程序crash
三.iOS面试题---runtime相关
- 数据结构:objc_object,objc_class,isa,class_data_bits_t,cache_t,method_t
- 对象,类对象,元类对象
- 消息传递
- 消息转发
一、数据结构:objc_object,objc_class,isa,class_data_bits_t,cache_t,method_t
objc_object(id)
isa_t,关于isa操作相关,弱引用相关,关联对象相关,内存管理相关objc_class (class) 继承自objc_object
Class superClass,cache_t cache,class_data_bits_t bits-
isa指针,共用体isa_t
isa指向
关于对象,其指向类对象。
关于类对象,其指向元类对象。
实例--(isa)-->class--(isa)-->MetaClasscache_t
用于快速查找方法执行函数,是可增量扩展的哈希表结构,是局部性原理的最佳运用
struct cache_t {
struct bucket_t *_buckets;//一个散列表,用来方法缓存,bucket_t类型,包含key以及方法实现IMP
mask_t _mask;//分配用来缓存bucket的总数
mask_t _occupied;//表明目前实际占用的缓存bucket的个数
}
struct bucket_t {
private:
cache_key_t _key;
IMP _imp;
}
- class_data_bits_t:对class_rw_t的封装
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;
char *demangledName;
}
Objc的类的属性、方法、以及遵循的协议都放在class_rw_t中,class_rw_t代表了类相关的读写信息,是对class_ro_t的封装,而class_ro_t代表了类的只读信息,存储了 编译器决定了的属性、方法和遵守协议
struct class_ro_t {
uint32_t flags;
uint32_t instanceStart;
uint32_t instanceSize;
#ifdef __LP64__
uint32_t reserved;
#endif
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;
method_list_t *baseMethods() const {
return baseMethodList;
}
};
- method_t
函数四要素:名称,返回值,参数,函数体
struct method_t {
SEL name; //名称
const char *types;//返回值和参数
IMP imp; //函数体
}
二、 对象,类对象,元类对象
类对象存储实例方法列表等信息。
-
元类对象存储类方法列表等信息。
superClass是一层层集成的,到最后NSObject的superClass是nil.而NSObject的isa指向根元类,这个根元类的isa指向它自己,而它的superClass是NSObject,也就是最后形成一个环,
三、消息传递void objc_msgSend(void /* id self, SEL op, ... */ ) void objc_msgSendSuper(void /* struct objc_super *super, SEL op, ... */ ) struct objc_super { /// Specifies an instance of a class. __unsafe_unretained _Nonnull id receiver; /// Specifies the particular superclass of the instance to message. #if !defined(__cplusplus) && !__OBJC2__ /* For compatibility with old objc-runtime.h header */ __unsafe_unretained _Nonnull Class class; #else __unsafe_unretained _Nonnull Class super_class; #endif /* super_class is the first class to search */ };
消息传递的流程:缓存查找-->当前类查找-->父类逐级查找
- 调用方法之前,先去查找缓存,看看缓存中是否有对应选择器的方法实现,如果有,就去调用函数,完成消息传递(缓存查找:给定值SEL,目标是查找对应bucket_t中的IMP,哈希查找)
- 如果缓存中没有,会根据当前实例的isa指针查找当前类对象的方法列表,看看是否有同样名称的方法 ,如果找到,就去调用函数,完成消息传递(当前类中查找:对于已排序好的方法列表,采用二分查找,对于没有排序好的列表,采用一般遍历)
- 如果当前类对象的方法列表没有,就会逐级父类方法列表中查找,如果找到,就去调用函数,完成消息传递(父类逐级查找:先判断父类是否为nil,为nil则结束,否则就继续进行缓存查找-->当前类查找-->父类逐级查找的流程)
- 如果一直查到根类依然没有查找到,则进入到消息转发流程中,完成消息传递
四、消息转发
+ (BOOL)resolveInstanceMethod:(SEL)sel;//为对象方法进行决议
+ (BOOL)resolveClassMethod:(SEL)sel;//为类方法进行决议
- (id)forwardingTargetForSelector:(SEL)aSelector;//方法转发目标
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector;
- (void)forwardInvocation:(NSInvocation *)anInvocation;
那么最后消息未能处理的时候,还会调用到
- (void)doesNotRecognizeSelector:(SEL)aSelector
这个方法,我们也可以在这个方法中做处理,避免掉crash,但是只建议在线上环境的时候做处理,实际开发过程中还要把异常抛出来
-
方法交换(Method-Swizzling)
+ (void)load { Method test = class_getInstanceMethod(self, @selector(test)); Method otherTest = class_getInstanceMethod(self, @selector(otherTest)); method_exchangeImplementations(test, otherTest); }
应用场景:替换系统的方法,比如viewDidLoad,viewWillAppear以及一些响应方法,来进行统计信息
-
动态添加方法
class_addMethod(self, sel, testImp, "v@:"); void testImp (void) { NSLog(@"testImp"); }
@dynamic 动态方法解析
动态运行时语言将函数决议推迟到运行时
编译时语言在编译期进行函数决议[obj foo]和objc_msgSend()函数之间有什么关系?
objc_msgSend()是[obj foo]的具体实现。在runtime中,objc_msgSend()是一个c函数,[obj foo]会被翻译成这样的形式objc_msgSend(obj, foo)。runtime是如何通过selector找到对应的IMP地址的?
缓存查找-->当前类查找-->父类逐级查找能否向编译后的类中增加实例变量?
不能。 编译后,该类已经完成了实例变量的布局,不能再增加实例变量。
但可以向动态添加的类中增加实例变量。
四.iOS---算法相关
- 字符串反转
- 链表反转
- 有序数组合并
- Hash算法
- 查找两个子视图的共同父视图
- 求无序数组当中的中位数
一、字符串反转
给定字符串 "hello,world",实现将其反转。输出结果:dlrow,olleh
- (void)charReverse
{
NSString * string = @"hello,world";
NSLog(@"%@",string);
NSMutableString * reverString = [NSMutableString stringWithString:string];
for (NSInteger i = 0; i < (string.length + 1)/2; i++) {
[reverString replaceCharactersInRange:NSMakeRange(i, 1) withString:[string substringWithRange:NSMakeRange(string.length - i - 1, 1)]];
[reverString replaceCharactersInRange:NSMakeRange(string.length - i - 1, 1) withString:[string substringWithRange:NSMakeRange(i, 1)]];
}
NSLog(@"reverString:%@",reverString);
//C
char ch[100];
memcpy(ch, [string cStringUsingEncoding:NSUTF8StringEncoding], [string length]);
//设置两个指针,一个指向字符串开头,一个指向字符串末尾
char * begin = ch;
char * end = ch + strlen(ch) - 1;
//遍历字符数组,逐步交换两个指针所指向的内容,同时移动指针到对应的下个位置,直至begin>=end
while (begin < end) {
char temp = *begin;
*(begin++) = *end;
*(end--) = temp;
}
NSLog(@"reverseChar[]:%s",ch);
}
二、链表反转
反转前:1->2->3->4->NULL
反转后:4->3->2->1->NULL
/** 定义一个链表 */
struct Node {
NSInteger data;
struct Node * next;
};
- (void)listReverse
{
struct Node * p = [self constructList];
[self printList:p];
//反转后的链表头部
struct Node * newH = NULL;
//头插法
while (p != NULL) {
//记录下一个结点
struct Node * temp = p->next;
//当前结点的next指向新链表的头部
p->next = newH;
//更改新链表头部为当前结点
newH = p;
//移动p到下一个结点
p = temp;
}
[self printList:newH];
}
/**
打印链表
@param head 给定链表
*/
- (void)printList:(struct Node *)head
{
struct Node * temp = head;
printf("list is : ");
while (temp != NULL) {
printf("%zd ",temp->data);
temp = temp->next;
}
printf("\n");
}
/** 构造链表 */
- (struct Node *)constructList
{
//头结点
struct Node *head = NULL;
//尾结点
struct Node *cur = NULL;
for (NSInteger i = 0; i < 10; i++) {
struct Node *node = malloc(sizeof(struct Node));
node->data = i;
//头结点为空,新结点即为头结点
if (head == NULL) {
head = node;
}else{
//当前结点的next为尾结点
cur->next = node;
}
//设置当前结点为新结点
cur = node;
}
return head;
}
三、有序数组合并
将有序数组 {1,4,6,7,9} 和 {2,3,5,6,8,9,10,11,12} 合并为
{1,2,3,4,5,6,6,7,8,9,9,10,11,12}
- (void)orderListMerge
{
int aLen = 5,bLen = 9;
int a[] = {1,4,6,7,9};
int b[] = {2,3,5,6,8,9,10,11,12};
[self printList:a length:aLen];
[self printList:b length:bLen];
int result[14];
int p = 0,q = 0,i = 0;//p和q分别为a和b的下标,i为合并结果数组的下标
//任一数组没有达到s边界则进行遍历
while (p < aLen && q < bLen) {
//如果a数组对应位置的值小于b数组对应位置的值,则存储a数组的值,并移动a数组的下标与合并结果数组的下标
if (a[p] < b[q]) result[i++] = a[p++];
//否则存储b数组的值,并移动b数组的下标与合并结果数组的下标
else result[i++] = b[q++];
}
//如果a数组有剩余,将a数组剩余部分拼接到合并结果数组的后面
while (++p < aLen) {
result[i++] = a[p];
}
//如果b数组有剩余,将b数组剩余部分拼接到合并结果数组的后面
while (q < bLen) {
result[i++] = b[q++];
}
[self printList:result length:aLen + bLen];
}
- (void)printList:(int [])list length:(int)length
{
for (int i = 0; i < length; i++) {
printf("%d ",list[i]);
}
printf("\n");
}
四、HASH算法
- 哈希表
例:给定值是字母a,对应ASCII码值是97,数组索引下标为97。
这里的ASCII码,就算是一种哈希函数,存储和查找都通过该函数,有效地提高查找效率。 -
在一个字符串中找到第一个只出现一次的字符。如输入"abaccdeff",输出'b'
字符(char)是一个长度为8的数据类型,因此总共有256种可能。每个字母根据其ASCII码值作为数组下标对应数组种的一个数字。数组中存储的是每个字符出现的次数。
- (void)hashTest
{
NSString * testString = @"hhaabccdeef";
char testCh[100];
memcpy(testCh, [testString cStringUsingEncoding:NSUTF8StringEncoding], [testString length]);
int list[256];
for (int i = 0; i < 256; i++) {
list[i] = 0;
}
char *p = testCh;
char result = '\0';
while (*p != result) {
list[*(p++)]++;
}
p = testCh;
while (*p != result) {
if (list[*p] == 1) {
result = *p;
break;
}
p++;
}
printf("result:%c",result);
}
五、查找两个子视图的共同父视图
思路:分别记录两个子视图的所有父视图并保存到数组中,然后倒序寻找,直至找到第一个不一样的父视图。
- (void)findCommonSuperViews:(UIView *)view1 view2:(UIView *)view2
{
NSArray * superViews1 = [self findSuperViews:view1];
NSArray * superViews2 = [self findSuperViews:view2];
NSMutableArray * resultArray = [NSMutableArray array];
int i = 0;
while (i < MIN(superViews1.count, superViews2.count)) {
UIView *super1 = superViews1[superViews1.count - i - 1];
UIView *super2 = superViews2[superViews2.count - i - 1];
if (super1 == super2) {
[resultArray addObject:super1];
i++;
}else{
break;
}
}
NSLog(@"resultArray:%@",resultArray);
}
- (NSArray <UIView *>*)findSuperViews:(UIView *)view
{
UIView * temp = view.superview;
NSMutableArray * result = [NSMutableArray array];
while (temp) {
[result addObject:temp];
temp = temp.superview;
}
return result;
}
六、求无序数组中的中位数
中位数:当数组个数n为奇数时,为(n + 1)/2,即是最中间那个数字;当n为偶数时,为(n/2 + (n/2 + 1))/2,即是中间两个数字的平均数。
首先要先去了解一些几种排序算法:iOS排序算法
思路:
- 1.排序算法+中位数
首先用冒泡排序、快速排序、堆排序、希尔排序等排序算法将所给数组排序,然后取出其中位数即可。 - 2.利用快排思想
五.iOS面试题-----内存管理、自动释放池与循环引用
- 内存布局
- 内存管理方案
- MRC(手动引用计数)和ARC(自动引用计数)
- 循环引用
一、内存布局
- 栈(stack):方法调用,局部变量等,是连续的,高地址往低地址扩展
- 堆(heap):通过alloc等分配的对象,是离散的,低地址往高地址扩展,需要我们手动控制
- 未初始化数据(bss):未初始化的全局变量等
- 已初始化数据(data):已初始化的全局变量等
- 代码段(text):程序代码
2、64bit和32bit下 long 和char*所占字节是不同的
char:1字节(ASCII 2[图片上传失败...(image-ce64d9-1627037665393)]
= 256个字符)
char*(即指针变量): 4个字节(32位的寻址空间是2[图片上传失败...(image-bb7380-1627037665393)]
,即32个bit,也就是4个字节。同理64位编译器为8个字节)
short int : 2个字节 范围 -2[图片上传失败...(image-e1dbca-1627037665393)]
~> 2[图片上传失败...(image-3e791c-1627037665393)]
即 -32768~>32767
int: 4个字节 范围 -2147483648~>2147483647
unsigned int : 4个字节
long: 4个字节 范围 和int一样 64位下8个字节,范围 -9223372036854775808~9223372036854775807
long long: 8个字节 范围 -9223372036854775808~9223372036854775807
unsigned long long: 8个字节 最大值:1844674407370955161
float: 4个字节
double: 8个字节。
3、static、const和sizeof关键字
static关键字
答:Static的用途主要有两个,一是用于修饰存储类型使之成为静态存储类型,二是用于修饰链接属性使之成为内部链接属性。
- 1、静态存储类型:
在函数内定义的静态局部变量,该变量存在内存的静态区,所以即使该函数运行结束,静态变量的值不会被销毁,函数下次运行时能仍用到这个值。
在函数外定义的静态变量——静态全局变量,该变量的作用域只能在定义该变量的文件中,不能被其他文件通过extern引用。
- 2、内部链接属性
静态函数只能在声明它的源文件中使用。
const关键字
- 1、声明常变量,使得指定的变量不能被修改。
const int a = 5;/*a的值一直为5,不能被改变*/
const int b; b = 10;/*b的值被赋值为10后,不能被改变*/
const int *ptr; /*ptr为指向整型常量的指针,ptr的值可以修改,但不能修改其所指向的值*/
int *const ptr;/*ptr为指向整型的常量指针,ptr的值不能修改,但可以修改其所指向的值*/
const int *const ptr;/*ptr为指向整型常量的常量指针,ptr及其指向的值都不能修改*/
- 2、修饰函数形参,使得形参在函数内不能被修改,表示输入参数。
如
int fun(const int a);或int fun(const char *str);
- 3、修饰函数返回值,使得函数的返回值不能被修改。
const char *getstr(void);使用:const *str= getstr();
const int getint(void); 使用:const int a =getint();
sizeof关键字
sizeof是在编译阶段处理,且不能被编译为机器码。sizeof的结果等于对象或类型所占的内存字节数。sizeof的返回值类型为size_t。
变量:int a; sizeof(a)为4;
指针:int *p; sizeof(p)为4;
数组:int b[10]; sizeof(b)为数组的大小,4*10;int c[0]; sizeof(c)等于0
-
结构体:struct (int a; char ch;)s1; sizeof(s1)为8 与结构体字节对齐有关。
对结构体求sizeof时,有两个原则:(1)展开后的结构体的第一个成员的偏移量应当是被展开的结构体中最大的成员的整数倍。 (2)结构体大小必须是所有成员大小的整数倍,这里所有成员计算的是展开后的成员,而不是将嵌套的结构体当做一个整体。
-
注意:不能对结构体中的位域成员使用sizeof
sizeof(void)等于1
sizeof(void *)等于4
二、内存管理方案
- taggedPointer :存储小对象如NSNumber。深入理解Tagged Pointer
- NONPOINTER_ISA(非指针型的isa):在64位架构下,isa指针是占64比特位的,实际上只有30多位就已经够用了,为了提高利用率,剩余的比特位存储了内存管理的相关数据内容。
- 散列表:复杂的数据结构,包括了引用计数表和弱引用表
通过SideTables()结构来实现的,SideTables()结构下,有很多SideTable的数据结构。
而sideTable当中包含了自旋锁,引用计数表,弱引用表。
SideTables()实际上是一个哈希表,通过对象的地址来计算该对象的引用计数在哪个sideTable中。
自旋锁:
- 自旋锁是“忙等”的锁。
- 适用于轻量访问。
引用计数表和弱引用表实际是一个哈希表,来提高查找效率。
三、MRC(手动引用计数)和ARC(自动引用计数)
1、MRC:alloc,retain,release,retainCount,autorelease,dealloc
2、ARC:
- ARC是LLVM和Runtime协作的结果
- ARC禁止手动调用retain,release,retainCount,autorelease关键字
- ARC新增weak,strong关键字
3、引用计数管理:
alloc: 经过一系列函数调用,最终调用了calloc函数,这里并没有设置引用计数为1
retain: 经过两次哈希查找,找到其对应引用计数值,然后将引用计数加1(实际是加偏移量)
release:和retain相反,经过两次哈希查找,找到其对应引用计数值,然后将引用计数减1
-
dealloc:
4、弱引用管理:
- 添加weak变量:通过哈希算法位置查找添加。如果查找对应位置中已经有了当前对象所对应的弱引用数组,就把新的弱引用变量添加到数组当中;如果没有,就创建一个弱引用数组,并将该弱引用变量添加到该数组中。
- 当一个被weak修饰的对象被释放后,weak对象怎么处理的?
清除weak变量,同时设置指向为nil。当对象被dealloc释放后,在dealloc的内部实现中,会调用弱引用清除的相关函数,会根据当前对象指针查找弱引用表,找到当前对象所对应的弱引用数组,将数组中的所有弱引用指针都置为nil。
5、自动释放池:
在当次runloop将要结束的时候调用objc_autoreleasePoolPop,并push进来一个新的AutoreleasePool
AutoreleasePoolPage是以栈为结点通过双向链表的形式组合而成,是和线程一一对应的。
内部属性有parent,child对应前后两个结点,thread对应线程 ,next指针指向栈中下一个可填充的位置。
- AutoreleasePool实现原理?
编译器会将 @autoreleasepool {} 改写为:
void * ctx = objc_autoreleasePoolPush;
{}
objc_autoreleasePoolPop(ctx);
- objc_autoreleasePoolPush:
把当前next位置置为nil,即哨兵对象,然后next指针指向下一个可入栈位置,
AutoreleasePool的多层嵌套,即每次objc_autoreleasePoolPush,实际上是不断地向栈中插入哨兵对象。 - objc_autoreleasePoolPop:
根据传入的哨兵对象找到对应位置。
给上次push操作之后添加的对象依次发送release消息。
回退next指针到正确的位置。
四、循环引用
循环引用的实质:多个对象相互之间有强引用,不能释放让系统回收。
如何解决循环引用?
1、避免产生循环引用,通常是将 strong 引用改为 weak 引用。
比如在修饰属性时用weak
在block内调用对象方法时,使用其弱引用,这里可以使用两个宏
#define WS(weakSelf) __weak __typeof(&*self)weakSelf = self; // 弱引用
#define ST(strongSelf) __strong __typeof(&*self)strongSelf = weakSelf; //使用这个要先声明weakSelf
还可以使用__block来修饰变量
在MRC下,__block不会增加其引用计数,避免了循环引用
在ARC下,__block修饰对象会被强引用,无法避免循环引用,需要手动解除。
2、在合适时机去手动断开循环引用。
通常我们使用第一种。
循环引用场景:
自循环引用
对象强持有的属性同时持有该对象-
相互循环引用
-
多循环引用
1、代理(delegate)循环引用属于相互循环引用
delegate 是iOS中开发中比较常遇到的循环引用,一般在声明delegate的时候都要使用弱引用 weak,或者assign,当然怎么选择使用assign还是weak,MRC的话只能用assign,在ARC的情况下最好使用weak,因为weak修饰的变量在释放后自动指向nil,防止野指针存在
2、NSTimer循环引用属于相互循环使用
在控制器内,创建NSTimer作为其属性,由于定时器创建后也会强引用该控制器对象,那么该对象和定时器就相互循环引用了。
如何解决呢?
这里我们可以使用手动断开循环引用:
如果是不重复定时器,在回调方法里将定时器invalidate并置为nil即可。
如果是重复定时器,在合适的位置将其invalidate并置为nil即可
3、block循环引用
一个简单的例子:
@property (copy, nonatomic) dispatch_block_t myBlock;
@property (copy, nonatomic) NSString *blockString;
- (void)testBlock {
self.myBlock = ^() {
NSLog(@"%@",self.blockString);
};
}
由于block会对block中的对象进行持有操作,就相当于持有了其中的对象,而如果此时block中的对象又持有了该block,则会造成循环引用。
解决方案就是使用__weak修饰self即可
__weak typeof(self) weakSelf = self;
self.myBlock = ^() {
NSLog(@"%@",weakSelf.blockString);
};
- 并不是所有block都会造成循环引用。
只有被强引用了的block才会产生循环引用
而比如dispatch_async(dispatch_get_main_queue(), ^{}),[UIView animateWithDuration:1 animations:^{}]这些系统方法等
或者block并不是其属性而是临时变量,即栈block
[self testWithBlock:^{
NSLog(@"%@",self);
}];
- (void)testWithBlock:(dispatch_block_t)block {
block();
}
还有一种场景,在block执行开始时self对象还未被释放,而执行过程中,self被释放了,由于是用weak修饰的,那么weakSelf也被释放了,此时在block里访问weakSelf时,就可能会发生错误(向nil对象发消息并不会崩溃,但也没任何效果)。
对于这种场景,应该在block中对 对象使用__strong修饰,使得在block期间对 对象持有,block执行结束后,解除其持有。
__weak typeof(self) weakSelf = self;
self.myBlock = ^() {
__strong __typeof(self) strongSelf = weakSelf;
[strongSelf test];
};
六.iOS面试题-----Block原理、Block变量截获、Block的三种形式、__block
什么是Block?
Block变量截获
Block的几种形式
一、什么是Block?
Block是将函数及其执行上下文封装起来的对象。
比如:
NSInteger num = 3;
NSInteger(^block)(NSInteger) = ^NSInteger(NSInteger n){
return n*num;
};
block(2);
通过clang -rewrite-objc WYTest.m命令编译该.m文件,发现该block被编译成这个形式:
NSInteger num = 3;
NSInteger(*block)(NSInteger) = ((NSInteger (*)(NSInteger))&__WYTest__blockTest_block_impl_0((void *)__WYTest__blockTest_block_func_0, &__WYTest__blockTest_block_desc_0_DATA, num));
((NSInteger (*)(__block_impl *, NSInteger))((__block_impl *)block)->FuncPtr)((__block_impl *)block, 2);
其中WYTest是文件名,blockTest是方法名,这些可以忽略。
其中__WYTest__blockTest_block_impl_0结构体为
struct __WYTest__blockTest_block_impl_0 {
struct __block_impl impl;
struct __WYTest__blockTest_block_desc_0* Desc;
NSInteger num;
__WYTest__blockTest_block_impl_0(void *fp, struct __WYTest__blockTest_block_desc_0 *desc, NSInteger _num, int flags=0) : num(_num) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
__block_impl结构体为
struct __block_impl {
void *isa;//isa指针,所以说Block是对象
int Flags;
int Reserved;
void *FuncPtr;//函数指针
};
block内部有isa指针,所以说其本质也是OC对象
block内部则为:
static NSInteger __WYTest__blockTest_block_func_0(struct __WYTest__blockTest_block_impl_0 *__cself, NSInteger n) {
NSInteger num = __cself->num; // bound by copy
return n*num;
}
所以说 Block是将函数及其执行上下文封装起来的对象
既然block内部封装了函数,那么它同样也有参数和返回值。
二、Block变量截获
1、局部变量截获 是值截获。 比如:
NSInteger num = 3;
NSInteger(^block)(NSInteger) = ^NSInteger(NSInteger n){
return n*num;
};
num = 1;
NSLog(@"%zd",block(2));
这里的输出是6而不是2,原因就是对局部变量num的截获是值截获。
同样,在block里如果修改变量num,也是无效的,甚至编译器会报错。
NSMutableArray * arr = [NSMutableArray arrayWithObjects:@"1",@"2", nil];
void(^block)(void) = ^{
NSLog(@"%@",arr);//局部变量
[arr addObject:@"4"];
};
[arr addObject:@"3"];
arr = nil;
block();
打印为1,2,3
局部对象变量也是一样,截获的是值,而不是指针,在外部将其置为nil,对block没有影响,而该对象调用方法会影响
2、局部静态变量截获 是指针截获。
static NSInteger num = 3;
NSInteger(^block)(NSInteger) = ^NSInteger(NSInteger n){
return n*num;
};
num = 1;
NSLog(@"%zd",block(2));
输出为2,意味着num = 1这里的修改num值是有效的,即是指针截获。
同样,在block里去修改变量m,也是有效的。
3、全局变量,静态全局变量截获:不截获,直接取值。
我们同样用clang编译看下结果。
static NSInteger num3 = 300;
NSInteger num4 = 3000;
-
(void)blockTest
{
NSInteger num = 30;static NSInteger num2 = 3;
__block NSInteger num5 = 30000;
void(^block)(void) = ^{
NSLog(@"%zd",num);//局部变量 NSLog(@"%zd",num2);//静态变量 NSLog(@"%zd",num3);//全局变量 NSLog(@"%zd",num4);//全局静态变量 NSLog(@"%zd",num5);//__block修饰变量
};
block();
}
编译后
struct __WYTest__blockTest_block_impl_0 {
struct __block_impl impl;
struct __WYTest__blockTest_block_desc_0* Desc;
NSInteger num;//局部变量
NSInteger *num2;//静态变量
__Block_byref_num5_0 *num5; // by ref//__block修饰变量
__WYTest__blockTest_block_impl_0(void *fp, struct __WYTest__blockTest_block_desc_0 *desc, NSInteger _num, NSInteger *_num2, __Block_byref_num5_0 *_num5, int flags=0) : num(_num), num2(_num2), num5(_num5->__forwarding) {
impl.isa = &_NSConcreteStackBlock;
impl.Flags = flags;
impl.FuncPtr = fp;
Desc = desc;
}
};
( impl.isa = &_NSConcreteStackBlock;这里注意到这一句,即说明该block是栈block)
可以看到局部变量被编译成值形式,而静态变量被编成指针形式,全局变量并未截获。而__block修饰的变量也是以指针形式截获的,并且生成了一个新的结构体对象:
struct __Block_byref_num5_0 {
void *__isa;
__Block_byref_num5_0 *__forwarding;
int __flags;
int __size;
NSInteger num5;
};
该对象有个属性:num5,即我们用__block修饰的变量。
这里__forwarding是指向自身的(栈block)。
一般情况下,如果我们要对block截获的局部变量进行赋值操作需添加__block
修饰符,而对全局变量,静态变量是不需要添加__block修饰符的。
另外,block里访问self或成员变量都会去截获self。
三、Block的几种形式
分为全局Block(_NSConcreteGlobalBlock)、栈Block(_NSConcreteStackBlock)、堆Block(_NSConcreteMallocBlock)三种形式
其中栈Block存储在栈(stack)区,堆Block存储在堆(heap)区,全局Block存储在已初始化数据(.data)区
1、不使用外部变量的block是全局block
比如:
NSLog(@"%@",[^{
NSLog(@"globalBlock");
} class]);
输出:
NSGlobalBlock
2、使用外部变量并且未进行copy操作的block是栈block
比如:
NSInteger num = 10;
NSLog(@"%@",[^{
NSLog(@"stackBlock:%zd",num);
} class]);
输出:
NSStackBlock
日常开发常用于这种情况:
[self testWithBlock:^{
NSLog(@"%@",self);
}];
-
(void)testWithBlock:(dispatch_block_t)block {
block();NSLog(@"%@",[block class]);
}
3、对栈block进行copy操作,就是堆block,而对全局block进行copy,仍是全局block
比如堆1中的全局进行copy操作,即赋值:
void (^globalBlock)(void) = ^{
NSLog(@"globalBlock");
};
NSLog(@"%@",[globalBlock class]);
输出:
NSGlobalBlock
仍是全局block
而对2中的栈block进行赋值操作:
NSInteger num = 10;
void (^mallocBlock)(void) = ^{
NSLog(@"stackBlock:%zd",num);
};
NSLog(@"%@",[mallocBlock class]);
输出:
NSMallocBlock
对栈blockcopy之后,并不代表着栈block就消失了,左边的mallock是堆block,右边被copy的仍是栈block
比如:
[self testWithBlock:^{
NSLog(@"%@",self);
}];
-
(void)testWithBlock:(dispatch_block_t)block
{
block();dispatch_block_t tempBlock = block;
NSLog(@"%@,%@",[block class],[tempBlock class]);
}
输出:
NSStackBlock,NSMallocBlock
即如果对栈Block进行copy,将会copy到堆区,对堆Block进行copy,将会增加引用计数,对全局Block进行copy,因为是已经初始化的,所以什么也不做。
另外,__block变量在copy时,由于__forwarding的存在,栈上的__forwarding指针会指向堆上的__forwarding变量,而堆上的__forwarding指针指向其自身,所以,如果对__block的修改,实际上是在修改堆上的__block变量。
即__forwarding指针存在的意义就是,无论在任何内存位置, 都可以顺利地访问同一个__block变量。
另外由于block捕获的__block修饰的变量会去持有变量,那么如果用__block修饰self,且self持有block,并且block内部使用到__block修饰的self时,就会造成多循环引用,即self持有block,block 持有__block变量,而__block变量持有self,造成内存泄漏。
比如:
__block typeof(self) weakSelf = self;
_testBlock = ^{
NSLog(@"%@",weakSelf);
};
_testBlock();
如果要解决这种循环引用,可以主动断开__block变量对self的持有,即在block内部使用完weakself后,将其置为nil,但这种方式有个问题,如果block一直不被调用,那么循环引用将一直存在。
所以,我们最好还是用__weak来修饰self
七.iOS面试题-----进程、线程、多进程、多线程、任务、队列、NSThread、GCD、NSOprationQueue...
-
进程
-
线程
-
进程和线程的关系
-
多进程
-
多线程
-
任务
-
队列
-
iOS中的多线程
一、 进程:
- 1.进程是一个具有一定独立功能的程序关于某次数据集合的一次运行活动,它是操作系统分配资源的基本单元.
- 2.进程是指在系统中正在运行的一个应用程序,就是一段程序的执行过程,我们可以理解为手机上的一个app.
- 3.每个进程之间是独立的,每个进程均运行在其专用且受保护的内存空间内,拥有独立运行所需的全部资源
二、 线程
- 1.程序执行流的最小单元,线程是进程中的一个实体.
- 2.一个进程要想执行任务,必须至少有一条线程.应用程序启动的时候,系统会默认开启一条线程,也就是主线程
三、 进程和线程的关系
- 1.线程是进程的执行单元,进程的所有任务都在线程中执行
- 2.线程是 CPU 分配资源和调度的最小单位
- 3.一个程序可以对应多个进程(多进程),一个进程中可有多个线程,但至少要有一条线程
- 4.同一个进程内的线程共享进程资源
四、 多进程
打开mac的活动监视器,可以看到很多个进程同时运行
- 进程是程序在计算机上的一次执行活动。当你运行一个程序,你就启动了一个进程。显然,程序是死的(静态的),进程是活的(动态的)。
- 进程可以分为系统进程和用户进程。凡是用于完成操作系统的各种功能的进程就是系统进程,它们就是处于运行状态下的操作系统本身;所有由用户启动的进程都是用户进程。进程是操作系统进行资源分配的单位。
- 进程又被细化为线程,也就是一个进程下有多个能独立运行的更小的单位。在同一个时间里,同一个计算机系统中如果允许两个或两个以上的进程处于运行状态,这便是多进程。
五、 多线程
1.同一时间,CPU只能处理1条线程,只有1条线程在执行。多线程并发执行,其实是CPU快速地在多条线程之间调度(切换)。如果CPU调度线程的时间足够快,就造成了多线程并发执行的假象
2.如果线程非常非常多,CPU会在N多线程之间调度,消耗大量的CPU资源,每条线程被调度执行的频次会降低(线程的执行效率降低)
3.多线程的优点:
能适当提高程序的执行效率
能适当提高资源利用率(CPU、内存利用率)4.多线程的缺点:
开启线程需要占用一定的内存空间(默认情况下,主线程占用1M,子线程占用512KB),如果开启大量的线程,会占用大量的内存空间,降低程序的性能
线程越多,CPU在调度线程上的开销就越大
程序设计更加复杂:比如线程之间的通信、多线程的数据共享
六、任务
就是执行操作的意思,也就是在线程中执行的那段代码。在 GCD 中是放在 block 中的。执行任务有两种方式:同步执行(sync)和异步执行(async)
- 同步(Sync):同步添加任务到指定的队列中,在添加的任务执行结束之前,会一直等待,直到队列里面的任务完成之后再继续执行,即会阻塞线程。只能在当前线程中执行任务(是当前线程,不一定是主线程),不具备开启新线程的能力。
- 异步(Async):线程会立即返回,无需等待就会继续执行下面的任务,不阻塞当前线程。可以在新的线程中执行任务,具备开启新线程的能力(并不一定开启新线程)。如果不是添加到主队列上,异步会在子线程中执行任务
七、队列
队列(Dispatch Queue):这里的队列指执行任务的等待队列,即用来存放任务的队列。队列是一种特殊的线性表,采用 FIFO(先进先出)的原则,即新任务总是被插入到队列的末尾,而读取任务的时候总是从队列的头部开始读取。每读取一个任务,则从队列中释放一个任务
在 GCD 中有两种队列:串行队列和并发队列。两者都符合 FIFO(先进先出)的原则。两者的主要区别是:执行顺序不同,以及开启线程数不同。
串行队列(Serial Dispatch Queue):
同一时间内,队列中只能执行一个任务,只有当前的任务执行完成之后,才能执行下一个任务。(只开启一个线程,一个任务执行完毕后,再执行下一个任务)。主队列是主线程上的一个串行队列,是系统自动为我们创建的-
并发队列(Concurrent Dispatch Queue):
同时允许多个任务并发执行。(可以开启多个线程,并且同时执行任务)。并发队列的并发功能只有在异步(dispatch_async)函数下才有效
八、iOS中的多线程
主要有三种:NSThread、NSoperationQueue、GCD
1. NSThread:轻量级别的多线程技术
是我们自己手动开辟的子线程,如果使用的是初始化方式就需要我们自己启动,如果使用的是构造器方式它就会自动启动。只要是我们手动开辟的线程,都需要我们自己管理该线程,不只是启动,还有该线程使用完毕后的资源回收
NSThread *thread = [[NSThread alloc] initWithTarget:self selector:@selector(testThread:) object:@"我是参数"];
// 当使用初始化方法出来的主线程需要start启动
[thread start];
// 可以为开辟的子线程起名字
thread.name = @"NSThread线程";
// 调整Thread的权限 线程权限的范围值为0 ~ 1 。越大权限越高,先执行的概率就会越高,由于是概率,所以并不能很准确的的实现我们想要的执行顺序,默认值是0.5
thread.threadPriority = 1;
// 取消当前已经启动的线程
[thread cancel];
// 通过遍历构造器开辟子线程
[NSThread detachNewThreadSelector:@selector(testThread:) toTarget:self withObject:@"构造器方式"];
- performSelector...只要是NSObject的子类或者对象都可以通过调用方法进入子线程和主线程,其实这些方法所开辟的子线程也是NSThread的另一种体现方式。
在编译阶段并不会去检查方法是否有效存在,如果不存在只会给出警告
//在当前线程。延迟1s执行。响应了OC语言的动态性:延迟到运行时才绑定方法
[self performSelector:@selector(aaa) withObject:nil afterDelay:1];
// 回到主线程。waitUntilDone:是否将该回调方法执行完在执行后面的代码,如果为YES:就必须等回调方法执行完成之后才能执行后面的代码,说白了就是阻塞当前的线程;如果是NO:就是不等回调方法结束,不会阻塞当前线程
[self performSelectorOnMainThread:@selector(aaa) withObject:nil waitUntilDone:YES];
//开辟子线程
[self performSelectorInBackground:@selector(aaa) withObject:nil];
//在指定线程执行
[self performSelector:@selector(aaa) onThread:[NSThread currentThread] withObject:nil waitUntilDone:YES]
需要注意的是:如果是带afterDelay的延时函数,会在内部创建一个 NSTimer,然后添加到当前线程的Runloop中。也就是如果当前线程没有开启runloop,该方法会失效。在子线程中,需要启动runloop(注意调用顺序)
[self performSelector:@selector(aaa) withObject:nil afterDelay:1];
[[NSRunLoop currentRunLoop] run];
而performSelector:withObject:只是一个单纯的消息发送,和时间没有一点关系。所以不需要添加到子线程的Runloop中也能执行
2、GCD 对比 NSOprationQueue
我们要明确NSOperationQueue与GCD之间的关系
GCD是面向底层的C语言的API,NSOpertaionQueue用GCD构建封装的,是GCD的高级抽象。
1、GCD执行效率更高,而且由于队列中执行的是由block构成的任务,这是一个轻量级的数据结构,写起来更方便
2、GCD只支持FIFO的队列,而NSOperationQueue可以通过设置最大并发数,设置优先级,添加依赖关系等调整执行顺序
3、NSOperationQueue甚至可以跨队列设置依赖关系,但是GCD只能通过设置串行队列,或者在队列内添加barrier(dispatch_barrier_async)任务,才能控制执行顺序,较为复杂
4、NSOperationQueue因为面向对象,所以支持KVO,可以监测operation是否正在执行(isExecuted)、是否结束(isFinished)、是否取消(isCanceld)
- 实际项目开发中,很多时候只是会用到异步操作,不会有特别复杂的线程关系管理,所以苹果推崇的且优化完善、运行快速的GCD是首选
- 如果考虑异步操作之间的事务性,顺序行,依赖关系,比如多线程并发下载,GCD需要自己写更多的代码来实现,而NSOperationQueue已经内建了这些支持
- 不论是GCD还是NSOperationQueue,我们接触的都是任务和队列,都没有直接接触到线程,事实上线程管理也的确不需要我们操心,系统对于线程的创建,调度管理和释放都做得很好。而NSThread需要我们自己去管理线程的生命周期,还要考虑线程同步、加锁问题,造成一些性能上的开销
- 更多关于GCD:2019 iOS面试题-----多线程相关之GCD、死锁、dispatch_barrier_async、dispatch_group_async、Dispatch Semaphore
八.iOS面试题-----多线程相关之GCD、死锁、dispatch_barrier_async、dispatch_group_async、Dispatch Semaphore、dispa...
-
GCD---同步/异步 ,串行/并发
-
死锁
-
GCD任务执行顺序
-
dispatch_barrier_async
-
dispatch_group_async
-
Dispatch Semaphore
-
延时函数(dispatch_after)
-
使用dispatch_once实现单例
一、GCD---队列
iOS中,有GCD、NSOperation、NSThread等几种多线程技术方案。
而GCD共有三种队列类型:
main queue:通过dispatch_get_main_queue()获得,这是一个与主线程相关的串行队列。
global queue:全局队列是并发队列,由整个进程共享。存在着高、中、低三种优先级的全局队列。调用dispath_get_global_queue并传入优先级来访问队列。
自定义队列:通过函数dispatch_queue_create创建的队列。
二、 死锁
死锁就是队列引起的循环等待
1、一个比较常见的死锁例子:主队列同步
- (void)viewDidLoad {
[super viewDidLoad];
dispatch_sync(dispatch_get_main_queue(), ^{
NSLog(@"deallock");
});
// Do any additional setup after loading the view, typically from a nib.
}
在主线程中运用主队列同步,也就是把任务放到了主线程的队列中。
同步对于任务是立刻执行的,那么当把任务放进主队列时,它就会立马执行,只有执行完这个任务,viewDidLoad才会继续向下执行。
而viewDidLoad和任务都是在主队列上的,由于队列的先进先出原则,任务又需等待viewDidLoad执行完毕后才能继续执行,viewDidLoad和这个任务就形成了相互循环等待,就造成了死锁。
想避免这种死锁,可以将同步改成异步dispatch_async,或者将dispatch_get_main_queue换成其他串行或并行队列,都可以解决。
2、同样,下边的代码也会造成死锁:
dispatch_queue_t serialQueue = dispatch_queue_create("test", DISPATCH_QUEUE_SERIAL);
dispatch_async(serialQueue, ^{
dispatch_sync(serialQueue, ^{
NSLog(@"deadlock");
});
});
外面的函数无论是同步还是异步都会造成死锁。
这是因为里面的任务和外面的任务都在同一个serialQueue队列内,又是同步,这就和上边主队列同步的例子一样造成了死锁
解决方法也和上边一样,将里面的同步改成异步dispatch_async,或者将serialQueue换成其他串行或并行队列,都可以解决
dispatch_queue_t serialQueue = dispatch_queue_create("test", DISPATCH_QUEUE_SERIAL);
dispatch_queue_t serialQueue2 = dispatch_queue_create("test", DISPATCH_QUEUE_SERIAL);
dispatch_async(serialQueue, ^{
dispatch_sync(serialQueue2, ^{
NSLog(@"deadlock");
});
});
这样是不会死锁的,并且serialQueue和serialQueue2是在同一个线程中的。
三、GCD任务执行顺序
1、串行队列先异步后同步
dispatch_queue_t serialQueue = dispatch_queue_create("test", DISPATCH_QUEUE_SERIAL);
NSLog(@"1");
dispatch_async(serialQueue, ^{
NSLog(@"2");
});
NSLog(@"3");
dispatch_sync(serialQueue, ^{
NSLog(@"4");
});
NSLog(@"5");
打印顺序是13245
原因是:
首先先打印1
接下来将任务2其添加至串行队列上,由于任务2是异步,不会阻塞线程,继续向下执行,打印3
然后是任务4,将任务4添加至串行队列上,因为任务4和任务2在同一串行队列,根据队列先进先出原则,任务4必须等任务2执行后才能执行,又因为任务4是同步任务,会阻塞线程,只有执行完任务4才能继续向下执行打印5
所以最终顺序就是13245。
这里的任务4在主线程中执行,而任务2在子线程中执行。
如果任务4是添加到另一个串行队列或者并行队列,则任务2和任务4无序执行(可以添加多个任务看效果)
2、performSelector
dispatch_async(dispatch_get_global_queue(0, 0), ^{
[self performSelector:@selector(test:) withObject:nil afterDelay:0];
});
这里的test方法是不会去执行的,原因在于
- (void)performSelector:(SEL)aSelector withObject:(nullable id)anArgument afterDelay:(NSTimeInterval)delay;
这个方法要创建提交任务到runloop上的,而gcd底层创建的线程是默认没有开启对应runloop的,所有这个方法就会失效。
而如果将dispatch_get_global_queue改成主队列,由于主队列所在的主线程是默认开启了runloop的,就会去执行(将dispatch_async改成同步,因为同步是在当前线程执行,那么如果当前线程是主线程,test方法也是会去执行的)。
四、dispatch_barrier_async
1、问:怎么用GCD实现多读单写?
多读单写的意思就是:可以多个读者同时读取数据,而在读的时候,不能去写入数据。并且,在写的过程中,不能有其他写者去写。即读者之间是并发的,写者与读者或其他写者是互斥的。
这里的写处理就是通过栅栏的形式去写。
就可以用dispatch_barrier_sync(栅栏函数)去实现
2、dispatch_barrier_sync的用法:
dispatch_queue_t concurrentQueue = dispatch_queue_create("test", DISPATCH_QUEUE_CONCURRENT);
for (NSInteger i = 0; i < 10; i++) {
dispatch_sync(concurrentQueue, ^{
NSLog(@"%zd",i);
});
}
dispatch_barrier_sync(concurrentQueue, ^{
NSLog(@"barrier");
});
for (NSInteger i = 10; i < 20; i++) {
dispatch_sync(concurrentQueue, ^{
NSLog(@"%zd",i);
});
}
这里的dispatch_barrier_sync上的队列要和需要阻塞的任务在同一队列上,否则是无效的。
从打印上看,任务0-9和任务任务10-19因为是异步并发的原因,彼此是无序的。而由于栅栏函数的存在,导致顺序必然是先执行任务0-9,再执行栅栏函数,再去执行任务10-19。
- dispatch_barrier_sync: Submits a barrier block object for execution and waits until that block completes.(提交一个栅栏函数在执行中,它会等待栅栏函数执行完)
-
dispatch_barrier_async: Submits a barrier block for asynchronous execution and returns immediately.(提交一个栅栏函数在异步执行中,它会立马返回)
而dispatch_barrier_sync和dispatch_barrier_async的区别也就在于会不会阻塞当前线程
比如,上述代码如果在dispatch_barrier_async后随便加一条打印,则会先去执行该打印,再去执行任务0-9和栅栏函数;而如果是dispatch_barrier_sync,则会在任务0-9和栅栏函数后去执行这条打印。
3、则可以这样设计多读单写:
- (id)readDataForKey:(NSString *)key
{
__block id result;
dispatch_sync(_concurrentQueue, ^{
result = [self valueForKey:key];
});
return result;
}
- (void)writeData:(id)data forKey:(NSString *)key
{
dispatch_barrier_async(_concurrentQueue, ^{
[self setValue:data forKey:key];
});
}
五、dispatch_group_async
场景:在n个耗时并发任务都完成后,再去执行接下来的任务。比如,在n个网络请求完成后去刷新UI页面。
dispatch_queue_t concurrentQueue = dispatch_queue_create("test1", DISPATCH_QUEUE_CONCURRENT);
dispatch_group_t group = dispatch_group_create();
for (NSInteger i = 0; i < 10; i++) {
dispatch_group_async(group, concurrentQueue, ^{
sleep(1);
NSLog(@"%zd:网络请求",i);
});
}
dispatch_group_notify(group, dispatch_get_main_queue(), ^{
NSLog(@"刷新页面");
});
深入理解GCD之dispatch_group
六、Dispatch Semaphore
GCD 中的信号量是指 Dispatch Semaphore,是持有计数的信号。
Dispatch Semaphore 提供了三个函数
1.dispatch_semaphore_create:创建一个Semaphore并初始化信号的总量
2.dispatch_semaphore_signal:发送一个信号,让信号总量加1
3.dispatch_semaphore_wait:可以使总信号量减1,当信号总量为0时就会一直等待(阻塞所在线程),否则就可以正常执行。
Dispatch Semaphore 在实际开发中主要用于:
- 保持线程同步,将异步执行任务转换为同步执行任务
- 保证线程安全,为线程加锁
1、保持线程同步:
dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);
__block NSInteger number = 0;
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
number = 100;
dispatch_semaphore_signal(semaphore);
});
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
NSLog(@"semaphore---end,number = %zd",number);
dispatch_semaphore_wait加锁阻塞了当前线程,dispatch_semaphore_signal解锁后当前线程继续执行
2、保证线程安全,为线程加锁:
在线程安全中可以将dispatch_semaphore_wait看作加锁,而dispatch_semaphore_signal看作解锁
首先创建全局变量
_semaphore = dispatch_semaphore_create(1);
注意到这里的初始化信号量是1。
- (void)asyncTask
{
dispatch_semaphore_wait(_semaphore, DISPATCH_TIME_FOREVER);
count++;
sleep(1);
NSLog(@"执行任务:%zd",count);
dispatch_semaphore_signal(_semaphore);
}
异步并发调用asyncTask
for (NSInteger i = 0; i < 100; i++) {
dispatch_async(dispatch_get_global_queue(0, 0), ^{
[self asyncTask];
});
}
然后发现打印是从任务1顺序执行到100,没有发生两个任务同时执行的情况。
原因如下:
在子线程中并发执行asyncTask,那么第一个添加到并发队列里的,会将信号量减1,此时信号量等于0,可以执行接下来的任务。而并发队列中其他任务,由于此时信号量不等于0,必须等当前正在执行的任务执行完毕后调用dispatch_semaphore_signal将信号量加1,才可以继续执行接下来的任务,以此类推,从而达到线程加锁的目的。
六、延时函数(dispatch_after)
dispatch_after能让我们添加进队列的任务延时执行,该函数并不是在指定时间后执行处理,而只是在指定时间追加处理到dispatch_queue
//第一个参数是time,第二个参数是dispatch_queue,第三个参数是要执行的block
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
NSLog(@"dispatch_after");
});
由于其内部使用的是dispatch_time_t管理时间,而不是NSTimer。
所以如果在子线程中调用,相比performSelector:afterDelay,不用关心runloop是否开启
七、使用dispatch_once实现单例
+ (instancetype)shareInstance {
static dispatch_once_t onceToken;
static id instance = nil;
dispatch_once(&onceToken, ^{
instance = [[self alloc] init];
});
return instance;
}
传送门:
iOS面试资料大全)