iOS 底层面试

iOS面试总结-进阶

2021-02-05  本文已影响0人  youlookdeliciou

[toc]
主要是一些视频笔记和面试时候常问到的问题记录。(持续更新)

Runtime

消息传递、消息转发

消息传递

objc_msgSend(obj, SEL/@selector(aMethod)/);
1、从消息缓存列表里面通过哈希表查找对应的方法(哈希冲突怎么办?应该是通过再哈希的方式解决的)
2、在当前类方法列表中查找
对于已排序好的列表,采用二分查找算法查找方法对应执行函数
对于没有排序的列表,采用一般遍历查找方法对应执行函数
3、从父类逐级查找
判断父类是否是nil
有父类
在缓存中查找
缓存中无,则从父类方法列表中查找,直至父类为nil,进入消息转发流程

消息转发

resolveInstanceMethod:
(resolveClassMethod:)
为类添加一个方法,返回YES

forwardingTargetForSelector:
返回一个其他对象去处理这个消息(备用receiver)

forwardInvocation:
如果上面两种情况没有执行,就会执行通过forwardInvocation进行消息转发

方法替换(Method-Swizzling)

Method Swizzing是发生在运行时的,在运行时将一个方法的实现替换成另一个方法的实现;
每个类都维护着一个方法列表,即methodList,methodList中有不同的方法,每个方法中包含了方法的SEL和IMP,方法交换就是将原本的SEL和IMP对应断开,并将SEL和新的IMP生成对应关系;

RunLoop

什么是RunLoop?

RunLoop是通过内部维护的事件循环消息/事件进行管理对象
**事件循环(Event Loop)
没有消息需要处理的时候,休眠以避免资源占用;【用户态】->【内核态】
有消息需要处理的时候,立刻被唤醒【内核态】->【用户态】

RunLoop的数据结构

Runloop和线程是一一对应的

image.png

Timer与RunLoop的面试题

问题:定时器有个RunLoop mode,默认是在defaultMode,scrollView滚动的时候,主线程的RunLoop会转到UITrackingRunLoopMode,这时候定时器就会失效
解决:将定时器添加到CommonMode上
思考:为什么?
NSRunloopCommonModes

Block

这篇文章讲的挺透彻
iOS-Block本质

什么是Block?

block的几种形式?

堆block(__NSMallocBlock__)
栈block(__NSStackBlock__),使用外部变量并且未进行copy操作的block是栈block
全局block(__NSGlobalBlock__)不使用外部变量的block是全局block

block变量截获?

一般block会在栈区,经过copy之后,会拷贝到堆区,栈区的block的__forwarding指针指向拷贝后的堆区的block,而堆区的__forwarding指针会指向自己

为什么要用__block修饰局部变量?
__block修饰之后的局部变量实际变成了一个结构体,它内部有一个isa指针,这个结构体会被block捕获,成为其成员变量;block内部修改的时候,实际是通过这个结构体的isa指针去修改所修饰的局部变量的值的

弱引用管理

如何添加一个weak变量到弱引用表

一个被声明为__weak的对象指针,经过编译器编译之后,调用objc_initweak(),经过一些列的函数调用(storeWeak()),最终在weak_register_no_lock()函数中进行弱引用变量的添加;具体添加的位置是通过哈希算法进行位置查找,如果说查找对应位置当中已经有当前对象对应的弱引用数组,那么就把新的弱引用变量添加到这个数组当中,如果没有,重新创建一个弱引用数组,然后第0个位置添加上最新的weak指针,后面的都初始化为0或者nil。

weak如何置nil

当一个对象被dealloc之后,在dealloc的内部实现当中,会调用弱引用清除的相关函数weak_clear_no_lock(),在这个函数内部实现当中会根据 当前对象指针 查找弱引用表,把当前对象相对应的弱引用(数组)都拿出来,遍历数组当中所有的弱引用指针,置为nil。

weak自动置nil的原理(简书1,做参考)

runtime维护着一个weak表即hash表,用于存储指向对象的weak指针
Weak表是Hash表,Key是所指对象的地址,Value是Weak指针地址的数组
以对象的地址作为key,去找weak指针
触发调用arr_clear_deallocating 函数 ,根据对象的地址将所有weak指针地址的数组,遍历数组把其中的数据置为nil。

weak自动置nil的原理(简书2,做参考)

一 、实现
runtime在注册类时,会布局一个weak表(hash表),key是所指对象的地址,value是weak指针的地址的数组;当对象释放时,层层调用后,通过arr_clear_deallocating释放;

二、weak实现原理步骤:通过clang可以分析源码;

objc_initWeak//初始化weak;

objc_storeWeak()//修更新指针指向,创建对应的弱引用表;

clearDeallocating//通过key找到weak数组,然后对数组里的weak指针置nil,把这个entry(入口,记录)从weak表删除;

自动释放池问题

- (void)viewDidLoad {
    [super viewDidLoad];
    NSMutableArray *array = [NSMutableArray array];
    NSLog(@"%@",array);
}

Q: array在什么时候释放?
A: 在当前RunLoop将要结束的时候调用AutoreleasePoolPage::pop()来对其进行释放。
(实际上在每一次的RunLoop循环当中都会在将要结束的时候对前一次创建的AutoreleasePool进行pop操作,同时会push进来一个新的AutoreleasePool)
问题拓展:
要回答这个问题需要知道RunLoop和AutoReleasePool的关系。
Runloop每次循环都是被一个AutoReleasePool包围着的,具体说每次Runloop循环将要结束的时候会释放当前runloop的内存占用。再创建好一个AutoReleasePool给下一次Runloop循环使用。(慕课网6-7

ViewDidLoad是在主线程执行,在该方法中创建的array会加入到当次RunLoop的AutoReleasePool中,array会在当前RunLoop将要结束的时候得到内存释放。

一般错误的回答都是viewDidLoad方法结束就释放了。

AutoreleasePool原理?

数据结构:是以为节点,通过双向链表的形式组合而成。和线程是一一对应的。
objc_autoreleasePoolPush()
objc_autoreleasePoolPop()
objc_autorelease()

AutoreleasePool为什么可以嵌套调用?

A:多层嵌套就是多次插入哨兵对象

AutoreleasePool使用场景?

在for循环中alloc图片数据等内存消耗较大的场景手动插入autoreleasePool

组件化

组件化的好处?

如何实现解耦?

https://www.jianshu.com/p/464a8f1ab949

CTMeditor

AvoidCrash

Foundation框架潜在的崩溃的危险比如:

/**
 *  提示崩溃的信息(控制台输出、通知)
 *
 *  @param exception   捕获到的异常
 *  @param defaultToDo 这个框架里默认的做法
 */
+ (void)noteErrorWithException:(NSException *)exception defaultToDo:(NSString *)defaultToDo {

    //堆栈数据
    NSArray *callStackSymbolsArr = [NSThread callStackSymbols];
    
    //获取在哪个类的哪个方法中实例化的数组  字符串格式 -[类名 方法名]  或者 +[类名 方法名]
    NSString *mainCallStackSymbolMsg = [AvoidCrash getMainCallStackSymbolMessageWithCallStackSymbols:callStackSymbolsArr];
    
    if (mainCallStackSymbolMsg == nil) {
        
        mainCallStackSymbolMsg = @"崩溃方法定位失败,请您查看函数调用栈来排查错误原因";
        
    }
    
    NSString *errorName = exception.name;
    NSString *errorReason = exception.reason;
    //errorReason 可能为 -[__NSCFConstantString avoidCrashCharacterAtIndex:]: Range or index out of bounds
    //将avoidCrash去掉
    errorReason = [errorReason stringByReplacingOccurrencesOfString:@"avoidCrash" withString:@""];
    
    NSString *errorPlace = [NSString stringWithFormat:@"Error Place:%@",mainCallStackSymbolMsg];
    
    NSString *logErrorMessage = [NSString stringWithFormat:@"\n\n%@\n\n%@\n%@\n%@\n%@",AvoidCrashSeparatorWithFlag, errorName, errorReason, errorPlace, defaultToDo];
    
    logErrorMessage = [NSString stringWithFormat:@"%@\n\n%@\n\n",logErrorMessage,AvoidCrashSeparator];
    AvoidCrashLog(@"%@",logErrorMessage);
    
    
    //请忽略下面的赋值,目的只是为了能顺利上传到cocoapods
    logErrorMessage = logErrorMessage;
    
    NSDictionary *errorInfoDic = @{
                                   key_errorName        : errorName,
                                   key_errorReason      : errorReason,
                                   key_errorPlace       : errorPlace,
                                   key_defaultToDo      : defaultToDo,
                                   key_exception        : exception,
                                   key_callStackSymbols : callStackSymbolsArr
                                   };
    
    //将错误信息放在字典里,用通知的形式发送出去
    dispatch_async(dispatch_get_main_queue(), ^{
        [[NSNotificationCenter defaultCenter] postNotificationName:AvoidCrashNotification object:nil userInfo:errorInfoDic];
    });
}

多线程相关问题

参考:https://www.jianshu.com/p/361e8a0a4e7e

NSThread - 轻量级别的多线程技术,需要我们自己管理线程

需要我们手动开辟子线程,如果使用init初始化方式则需要手动启动,如果使用构造器方式初始化则会自动启动。

  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:withObject:afterDelay:会在内部创建一个NSTimer,然后添加到当前的RunLoop中,如果当前线程没有开启RunLoop(子线程默认没有开启RunLoop),该方法会失效

[self performSelector:@selector(aaa) withObject:nil afterDelay:1];
[[NSRunLoop currentRunLoop] run];

performSelector:withObject:没有添加timer,所以不需要添加子线程RunLoop也可以执行

GCD对比NSOperationQueue

GCD是面向底层的C语言的API,NSOpertaionQueue用GCD构建封装的,是GCD的高级抽象。

它们的区别

Q:假设有这么场景:有网络请求A、网络请求B,需要AB执行完之后继续进行下一步操作,怎么使用GCD实现?
A:

  1. 信号量(dispatch_semaphore)
- (void)GCD_Semaphore {
    dispatch_semaphore_t sem = dispatch_semaphore_create(0);
    NSLog(@"1");
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        NSLog(@"task1, %@",[NSThread currentThread]);
        sleep(1);
        dispatch_semaphore_signal(sem);
    });
    dispatch_semaphore_wait(sem, DISPATCH_TIME_FOREVER);
    
    NSLog(@"2");
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        NSLog(@"task2, %@",[NSThread currentThread]);
        sleep(1);
        dispatch_semaphore_signal(sem);
    });
    dispatch_semaphore_wait(sem, DISPATCH_TIME_FOREVER);
    
    NSLog(@"3, %@",[NSThread currentThread]);
}

打印结果

2021-01-05 23:15:33 1
2021-01-05 23:15:33 task1, <NSThread: 0x600000eecd00>{number = 3, name = (null)}
2021-01-05 23:15:34 2
2021-01-05 23:15:34 task2, <NSThread: 0x600000eecd00>{number = 3, name = (null)}
2021-01-05 23:15:35 3, <NSThread: 0x600000eb01c0>{number = 1, name = main}

这里的打印结果是1->task1->2->task2->3顺序执行,相当于加锁?

  1. dispatch_group(基于dispatch_semaphore实现的)
- (void)GCD_Group {
    
    dispatch_group_t group = dispatch_group_create();
    dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
    
    dispatch_group_enter(group);
    dispatch_group_async(group, queue, ^{
        NSLog(@"task1");
        dispatch_group_leave(group);
    });
    
    dispatch_group_enter(group);
    dispatch_group_async(group, queue, ^{
        NSLog(@"task2");
        dispatch_group_leave(group);
    });
    
    dispatch_group_notify(group, queue, ^{
        NSLog(@"notify");
    });
    
    NSLog(@"===");
    
}
  1. dispatch_barrier_async(同时也可以用来实现多读单写、加锁、设置最大线程数)
- (void)GCD_barrier {
    
    dispatch_queue_t queue = dispatch_queue_create("barrier_queue", DISPATCH_QUEUE_CONCURRENT);
    // 注意dispatch_barrier_async只在自己创建的并发队列中才有效,在global_queue,串行队列上效果跟dispatch_(a)sync一样
//    dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
    
    dispatch_async(queue, ^{
        NSLog(@"task1");
    });
    dispatch_async(queue, ^{
        NSLog(@"task2");
    });
    
    dispatch_barrier_async(queue, ^{
        NSLog(@"barrier");
    });
    NSLog(@"===");
    dispatch_async(queue, ^{
        NSLog(@"task3");
    });
    dispatch_async(queue, ^{
        NSLog(@"task4");
    });
}

多读单写

- (id)dataForKey:(NSString *)key {
    __block id data;
    //同步读取指定数据
    dispatch_sync(self.concurrentQueue, ^{
        data = [self.dict objectForKey:key];
    });
    return data;
}
- (void)setData:(id)data forKey:(NSString *)key {
    // 异步栅栏调用设置数据
    dispatch_barrier_async(self.concurrentQueue, ^{
        [self.dict setObject:data forKey:key];
    });
}

单例模式

这篇文章介绍的还不错
https://www.jianshu.com/p/a92c0283f243

什么是单例模式?

简单来说,一个单例类,在整个程序中只有一个实例,并且提供了类方法供全局调用,在编译时初始化这个类,然后一直保存在内存中,直到App退出时由系统自动释放这一部分内存

系统为我们提供的单例类有哪些?

单例的存放位置

全局区

变量的存放位置

位置 存放的变量
临时变量(由编译器管理自动创建/分配/释放的,栈中的内存被调用时处于存储空间中,调用完毕后由系统系统自动释放内存)
通过alloc、calloc、malloc或new申请内存,由开发者手动在调用之后通过free或delete释放内存。动态内存的生存期可以由我们决定,如果我们不释放内存,程序将在最后才释放掉动态内存,在ARC模式下,由系统自动管理。
全局区域 静态变量(编译时分配,APP结束时由系统释放)
常量 常量(编译时分配,APP结束时由系统释放)
代码区 存放代码

创建一个单例的方式

单例注意事项-保证单例只被初始化一次

  1. 对alloc、new、copy、mutableCopy的处理
    因为alloc] init 和 new都是调用的+ (instancetype)allocWithZone:(struct _NSZone *)zone方法,那么我们可以重写这个方法
+ (instancetype)allocWithZone:(struct _NSZone *)zone {
    NSLog(@"allocWithZone");
    @synchronized (self) {
        if (instance == nil) {
            instance = [super allocWithZone:zone];
            return instance;
        }
    }
    return nil;// 这里返回nil,那么后面初始化的对象就是nil了,返回instance的话其实就是同一个单例对象了
}
  1. 直接禁用对应的方法
+(instancetype) new __attribute__((unavailable("OneTimeClass类只能初始化一次")));
-(instancetype) copy __attribute__((unavailable("OneTimeClass类只能初始化一次")));
-(instancetype) mutableCopy  __attribute__((unavailable("OneTimeClass类只能初始化一次")));

(用NS_UNAVAILABLE也可以,但是这个就没有提示了)

还要了解一下FMDB

NSMutableArray数据结构分析

普通C数组是是一段能被方便读写的连续内存空间,使用一段线性内存空间的一个最明显的缺点是,在下标0插入一个元素时,需要移动其它元素,即memmove的原理:
https://blog.csdn.net/qq_27909209/article/details/82689322

image.png
移除元素时同理也要移动其它元素;
当数组非常大的时候可能就会出现问题。
NSMutableArray是一个类簇,[NSMutableArray new]实际返回的是__NSArrayM
(lldb) po [[ NSMutableArray new] class]
__NSArrayM

__NSArrayM使用了环形缓冲区 (circular buffer),这个数据结构相当简单,只是比常规数组或缓冲区复杂点。环形缓冲区的内容能在到达任意一端时绕向另一端。
环形缓冲区有一些非常酷的属性。尤其是,除非缓冲区满了,否则在任意一端插入或删除均不会要求移动任何内存。我们来分析这个类如何充分利用环形缓冲区来使得自身比 C 数组强大得多。我们在这里知道了几个有趣的东西:在删除的时候不会清除指针。最有意思的一点,如果我们在中间进行插入或者删除,只会移动最少的一边的元素。

NSDictionary数据结构

在内部,字典使用哈希表来组织其存储,并在给定相应键的情况下快速访问值

Crash类型

关于RunLoop防止崩溃

https://cloud.tencent.com/developer/article/1192474
这还有一篇文章可以参考(关于Crash收集)·
http://www.cocoachina.com/articles/12301

图像显示原理

CPU生成位图(bitmap)经由总线在合适的时机传给GPU;GPU拿到位图之后会做相应位图的渲染,包括纹理的合成,之后把结果放到帧缓冲区(Frame Buffer),由视频控制器,根据VSync信号在指定时间之前去提取帧缓冲区当中的内容,最终显示到手机屏幕上。

image.png

如何定位内存泄漏?

冷启动

pre-main

1、减少动态库、合并一些动态库(定期清理不必要的动态库)
2、减少Objc类、分类的数量、减少Selector数量(定期清理不必要的类、分类)
3、减少C++虚函数数量
4、Swift尽量使用struct
5、用+initialize方法和dispatch_once取代所有的attribute((constructor))、>C++静态构造器、Objc的+load

main

1、在不影响用户体验的前提下,尽可能将一些操作延迟,不要全部都放在didFinishLaunching方法中
2、监控、埋点、基础功能设置 在willFinishLaunching
3、定位、网络配置、基础SDK 、必须的数据 在 didFinishLaunching

首页渲染

1、避免使用xib
2、首页一般关联业务较多,优先请求和渲染用户可见的页面
3、业务组件,业务相关配置等,在首页渲染完成之后

内存管理方案

NONPOINTER_ISA
散列表
TaggedPointer

上一篇 下一篇

猜你喜欢

热点阅读