i面试题面试

2021 iOS面试题大全---全方面剖析面试(一)

2021-07-24  本文已影响0人  ios南方

(答案不唯一,仅供参考,文章最后有福利)

一. iOS面试题---UI相关:事件传递,图像显示,性能优化,离屏渲染


一、UIView与CALayer

image

<单一职责原则>
UIView为CALayer提供内容,以及负责处理触摸等事件,参与响应链
CALayer负责显示内容contents

二、事件传递与视图响应链 :

- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event;

- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event;

image image

如果事件一直传递到UIAppliction还是没处理,那就会忽略掉

三、图像显示原理

image

1.CPU:输出位图
2.GPU :图层渲染,纹理合成
3.把结果放到帧缓冲区(frame buffer)中
4.再由视频控制器根据vsync信号在指定时间之前去提取帧缓冲区的屏幕显示内容
5.显示到屏幕上

image

CPU工作
1.Layout: UI布局,文本计算
2.Display: 绘制
3.Prepare: 图片解码
4.Commit:提交位图

GPU渲染管线(OpenGL)
顶点着色,图元装配,光栅化,片段着色,片段处理

四、UI卡顿掉帧原因

image

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绘制原理

image image

异步绘制:
[self.layer.delegate displayLayer: ]
代理负责生成对应的bitmap
设置该bitmap作为该layer.contents属性的值

image

七、离屏渲染

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)

image

代理是一种设计模式,以@protocol形式体现,一般是一对一传递。
一般以weak关键词以规避循环引用。

四、通知(NSNotification)
使用观察者模式来实现的用于跨层传递信息的机制。传递方式是一对多的。

五、KVO (Key-value observing)
KVO是观察者模式的另一实现。
使用了isa混写(isa-swizzling)来实现KVO

image

使用setter方法改变值KVO会生效,使用setValue:forKey即KVC改变值KVO也会生效,因为KVC会去调用setter方法

- (void)setValue:(id)value
{
    [self willChangeValueForKey:@"key"];

    [super setValue:value];

    [self didChangeValueForKey:@"key"];
}

六、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>方法的话,会按照_key,_iskey,key,iskey的顺序搜索成员并进行赋值操作。

如果开发者想让这个类禁用KVC,那么重写+ (BOOL)accessInstanceVariablesDirectly方法让其返回NO即可,这样的话如果KVC没有找到set<Key>:属性名时,会直接用setValue:forUndefinedKey:方法。

当调用valueForKey:@”name“的代码时,KVC对key的搜索方式不同于setValue:属性值 forKey:@”name“,其搜索方式如下:

七、属性关键字
1.读写权限:readonly,readwrite(默认)
2.原子性: atomic(默认),nonatomic。atomic读写线程安全,但效率低,而且不是绝对的安全,比如如果修饰的是数组,那么对数组的读写是安全的,但如果是操作数组进行添加移除其中对象的还,就不保证安全了。
3.引用计数:

image

可变对象的copy和mutableCopy都是深拷贝
不可变对象的copy是浅拷贝,mutableCopy是深拷贝
copy方法返回的都是不可变对象

三.iOS面试题---runtime相关


一、数据结构:objc_object,objc_class,isa,class_data_bits_t,cache_t,method_t

image
 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;
 }

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;
    }
  };

struct method_t {
  SEL name;           //名称
  const char *types;//返回值和参数
  IMP imp;              //函数体
}

二、 对象,类对象,元类对象

消息传递的流程:缓存查找-->当前类查找-->父类逐级查找

四、消息转发

+ (BOOL)resolveInstanceMethod:(SEL)sel;//为对象方法进行决议
+ (BOOL)resolveClassMethod:(SEL)sel;//为类方法进行决议
- (id)forwardingTargetForSelector:(SEL)aSelector;//方法转发目标
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector;
- (void)forwardInvocation:(NSInvocation *)anInvocation;

image

那么最后消息未能处理的时候,还会调用到
- (void)doesNotRecognizeSelector:(SEL)aSelector这个方法,我们也可以在这个方法中做处理,避免掉crash,但是只建议在线上环境的时候做处理,实际开发过程中还要把异常抛出来

应用场景:替换系统的方法,比如viewDidLoad,viewWillAppear以及一些响应方法,来进行统计信息

四.iOS---算法相关


一、字符串反转
给定字符串 "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算法

- (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排序算法
思路:

五.iOS面试题-----内存管理、自动释放池与循环引用


一、内存布局

image
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的用途主要有两个,一是用于修饰存储类型使之成为静态存储类型,二是用于修饰链接属性使之成为内部链接属性。

在函数内定义的静态局部变量,该变量存在内存的静态区,所以即使该函数运行结束,静态变量的值不会被销毁,函数下次运行时能仍用到这个值。

在函数外定义的静态变量——静态全局变量,该变量的作用域只能在定义该变量的文件中,不能被其他文件通过extern引用。

静态函数只能在声明它的源文件中使用。

const关键字
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及其指向的值都不能修改*/

int fun(const int a);或int fun(const char *str);

const char *getstr(void);使用:const *str= getstr();

const int getint(void);  使用:const int a =getint();

sizeof关键字

sizeof是在编译阶段处理,且不能被编译为机器码。sizeof的结果等于对象或类型所占的内存字节数。sizeof的返回值类型为size_t。

二、内存管理方案

自旋锁:

引用计数表和弱引用表实际是一个哈希表,来提高查找效率。

三、MRC(手动引用计数)和ARC(自动引用计数)

1、MRC:alloc,retain,release,retainCount,autorelease,dealloc
2、ARC:
3、引用计数管理:
4、弱引用管理:
5、自动释放池:

在当次runloop将要结束的时候调用objc_autoreleasePoolPop,并push进来一个新的AutoreleasePool

AutoreleasePoolPage是以栈为结点通过双向链表的形式组合而成,是和线程一一对应的。
内部属性有parent,child对应前后两个结点,thread对应线程 ,next指针指向栈中下一个可填充的位置。

编译器会将 @autoreleasepool {} 改写为:

void * ctx = objc_autoreleasePoolPush;
    {}
objc_autoreleasePoolPop(ctx);

四、循环引用

循环引用的实质:多个对象相互之间有强引用,不能释放让系统回收。
如何解决循环引用?

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);
 };

[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;

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);
}];

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);

}];

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...


一、 进程:

二、 线程

三、 进程和线程的关系

四、 多进程

打开mac的活动监视器,可以看到很多个进程同时运行

image

五、 多线程

六、任务

就是执行操作的意思,也就是在线程中执行的那段代码。在 GCD 中是放在 block 中的。执行任务有两种方式:同步执行(sync)和异步执行(async)

七、队列

队列(Dispatch Queue):这里的队列指执行任务的等待队列,即用来存放任务的队列。队列是一种特殊的线性表,采用 FIFO(先进先出)的原则,即新任务总是被插入到队列的末尾,而读取任务的时候总是从队列的头部开始读取。每读取一个任务,则从队列中释放一个任务
在 GCD 中有两种队列:串行队列和并发队列。两者都符合 FIFO(先进先出)的原则。两者的主要区别是:执行顺序不同,以及开启线程数不同。

八、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:@"构造器方式"];

      //在当前线程。延迟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)

image

八.iOS面试题-----多线程相关之GCD、死锁、dispatch_barrier_async、dispatch_group_async、Dispatch Semaphore、dispa...


一、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实现多读单写?

多读单写的意思就是:可以多个读者同时读取数据,而在读的时候,不能去写入数据。并且,在写的过程中,不能有其他写者去写。即读者之间是并发的,写者与读者或其他写者是互斥的。

image

这里的写处理就是通过栅栏的形式去写。
就可以用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。

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面试资料大全)

上一篇下一篇

猜你喜欢

热点阅读