iOS面试总结

目录

  • 1.main函数的autoreleasepool有什么作用?
  • 2.HTTPS的原理是什么,验证原理又是什么?
  • 3.iOS触摸时间传递及响应原理
  • 4.load、initialize详解与区别
  • 5.imageWithContentsOfFile:imageNamed
  • 6.FMDB怎么保证线程安全的
  • 7.MLeaksFinder原理
  • 8.FMDB数据库实现多读单写
  • 9.HTTP的TCP协议和Socket的TCP协议有何不同?

1、main函数的autoreleasepool有什么作用?

autoreleasepool是自动释放池,加入自动释放池的对象,初始化时调用autorelease方法,那么在autoreleasepool语句块结束时,对象会被释放,从而防止占用过多内存,在ARC机制下,autorelease方法由系统自动调用。main函数中的autoreleasepool的作用,就是当主线程运行循环结束时,释放所有对象。

2、HTTPS的原理是什么,验证原理又是什么?

  • HTTP的缺点:通信使用明文、不验证通信方的身份,无法验证报文的完整性
  • HTTPS的实质:SSL+ HTTP
  • HTTPS加密原理:非对称加密(交换密钥)+ 对称加密(数据通信)
  • HTTPS的验证流程如下:


    iOS面试总结_第1张图片
    https验证流程.png

具体参考:https://www.jianshu.com/p/f088fe60977e

3、iOS触摸时间传递及响应原理

iOS系统触摸事件响应链如下:

  • 事件触发:主屏幕触摸事件 --> 系统进程 --> App进程接收系统触摸事件,触发source1回调 --> App进程触发source0回调 --> source0回调将触摸事件添加到Application事件队列
  • 寻找最佳响应者:首先通过hitTest:withEvent:寻找最佳响应者,顺序为:UIApplication --> UIWindow --> UIViewController --> UIViewController.view --> view
  • 响应事件:再者通过touchesBegan:withEvent:来响应和传递这个触摸事件。默认顺序为:最佳响应者 --> 最佳响应者的父View --> UIViewController.view --> UIViewController --> UIWindow --> UIApplication --> UIApplicationDelegate
  • 触摸事件优先级:UIGestureRecognizer > UIControl > 父view的UIGestureRecognizer > UIResponder

4、load、initialize详解与区别

load实现原理

  • App启动,类第一次加载进内存的时候,会调用 + load 方法,无需导入,无需使用
  • 每个类、分类的 + load 在程序运行过程中只会执行一次
  • + load走的不是消息发送的 objc_msgSend 调用,而是找到 + load 函数的地址,直接调用

调用顺序

  • 先调用宿主类的+ load函数
    • 按照编译先后顺序调用(先编译,先调用)
    • 调用子类的+load之前会先调用父类的+load
  • 再调用分类的+ load函数
    • 按照编译先后的顺序调用(先编译,先调用)

Initialize实现原理

  • 类第一次接收到消息的时候,会调用该方法,需导入,并使用
  • + Initialize走的是消息发送的objc_msgSend调用

initialize 与 load 的区别

  • load是类第一次加载的时候调用,initialize是类第一次接收到消息的时候调用,每个类只会initialize一次(当子类没有实现initialize方法时,会调用父类的initialize)方法
  • load 和 initialize,加载or调用的时候,都会先调用父类对应的 load or initialize 方法,再调用自己本身的;
  • load 和 initialize 都是系统自动调用的话,都只会调用一次
  • 调用方式也不一样,load 是根据函数地址直接调用,initialize 是通过objc_msgSend
  • 调用时刻,load是runtime加载类、分类的时候调用(只会调用一次)
  • 调用顺序:
    • load
      • 先调用类的load
        1)先编译的类,优先调用load
        2)调用子类的load之前,会先调用父类的load(父类的load已经调用过了就不会再调用了)
      • 再调用分类的load
    • initialize
      • 先初始化父类
      • 再初始化子类(可能最终调用的是父类的初始化方法)

5、imageWithContentsOfFile:imageNamed

  • imageWithContentsOfFile:

    • 图片创建是通过读取文件数据得到的,读取一次文件数据就会产生一次NSData以及产生一个UIImage, 当图片创建好后销毁对应的NSData, 当UIImage的引用计数器变为0的时候自动销毁UIImage.这样的话就可以保证图片不会长期地存在在内存中.

    • 由于是从沙盒中加载,加载速度偏慢

    • 当同时加载一张图片的时候,占用的内存将会更大

      当我们需要图片的时候就会去沙盒中读取这个图片文件, 转换成UIImage对象来使用. 现在假设一种场景:
      [email protected] 图片占用 5kb 的内存
      [email protected] 在多个界面都用到, 且有7处会同时显示这个图片
      通过代码分析就可以知道 Resource 这个方式在这个情景下会占用 5kb/个 X 7个 = 35kb 内存. 然而, 在 ImageAssets 方式下, 全部取自字典缓存中的UIImage, 无论有几处显示图片, 都只会占用 5kb/个 X 1个 = 5kb 内存. 此时 Resource 占用内存将会更大.

  • imageNamed:

    • imageNamed: 也是从图片文件中读取图片数据转为 UIImage, 只不过这些图片数据都打包在 ImageAssets 中. 还有一个最大的区别就是图片缓存. 相当于有一个字典, key 是图片名, value是图片对象. 调用imageNamed:方法时候先从这个字典里取, 如果取到就直接返回, 如果取不到再去文件中创建, 然后保存到这个字典后再返回. 由于字典的key和value都是强引用, 所以一旦创建后的图片永不销毁.
    • 当图片从字典(内存)中取时,图片加载速度会更快。

6.FMDB怎么保证线程安全的

FMDB源码主要由一下几个部分组成:

  • FMDatabase:表示一个单独的SQLite数据库操作实例,通过它可以对数据库进行增删改查等操作。
  • FMResultSet:查询结果,表示FMDatabase执行查询之后的结果集。
  • FMDatabaseQueue:支持在串行队列里访问FMDatabase,保证线程安全。
  • FMDatabaseAdditions:扩展FMDatabaseAdditions类,新增对查询结果只返回单个值的方法进行简化,对表、列是否存在,版本号,校验SQL等功能。
  • FMDatabasePool: 使用任务池的形式,对多线程的操作提供支持。

直接通过FMDatabase操作数据库,在多线程环境下是容易出现线程安全问题的。
而通过FMDatabaseQueue访问FMDatabase再操作数据库是线程安全的,因为FMDatabaseQueue里面封装了一个串行队列,在串行队列里访问可以始终保持在一个线程下访问。FMDatabaseQueue初始化串行队列源码如下:

- (instancetype)initWithPath:(NSString*)aPath flags:(int)openFlags vfs:(NSString *)vfsName {
    self = [super init];
    
    if (self != nil) {
        
        _db = [[[self class] databaseClass] databaseWithPath:aPath];
        FMDBRetain(_db);
        
#if SQLITE_VERSION_NUMBER >= 3005000
        BOOL success = [_db openWithFlags:openFlags vfs:vfsName];
#else
        BOOL success = [_db open];
#endif
        if (!success) {
            NSLog(@"Could not create database queue for path %@", aPath);
            FMDBRelease(self);
            return 0x00;
        }
        
        _path = FMDBReturnRetained(aPath);
        // 初始化一个串行队列,以后访问数据库就在串行队列里访问
        _queue = dispatch_queue_create([[NSString stringWithFormat:@"fmdb.%@", self] UTF8String], NULL);
        dispatch_queue_set_specific(_queue, kDispatchQueueSpecificKey, (__bridge void *)self, NULL);
        _openFlags = openFlags;
        _vfsName = [vfsName copy];
    }
    
    return self;
}

通过串行队列访问FMDatabase源码如下:

- (void)inDatabase:(__attribute__((noescape)) void (^)(FMDatabase *db))block {
#ifndef NDEBUG
    /* Get the currently executing queue (which should probably be nil, but in theory could be another DB queue
     * and then check it against self to make sure we're not about to deadlock. */
    /*
     使用dispatch_get_specific来查看当前queue是否是之前设定的那个_queue,
     如果是的话,那么使用kDispatchQueueSpecificKey作为参数传给dispatch_get_specific的话,
     返回的值不为空,而且返回值应该就是上面initWithPath:函数中绑定的那个FMDatabaseQueue对象。
     有人说除了当前queue还有可能有其他什么queue?
     这就是FMDatabaseQueue的用途,你可以创建多个FMDatabaseQueue对象来并发执行不同的SQL语句。

     另外为啥要判断是不是当前执行的这个queue?是为了防止死锁!

     */
    FMDatabaseQueue *currentSyncQueue = (__bridge id)dispatch_get_specific(kDispatchQueueSpecificKey);
    assert(currentSyncQueue != self && "inDatabase: was called reentrantly on the same queue, which would lead to a deadlock");
#endif
    
    FMDBRetain(self);

    // 利用GCD同步函数派发串行队列,在串行队列里访问FMDatabase
    dispatch_sync(_queue, ^() {
        
        FMDatabase *db = [self database];
        
        block(db);
        
        if ([db hasOpenResultSets]) {
            NSLog(@"Warning: there is at least one open result set around after performing [FMDatabaseQueue inDatabase:]");
            
#if defined(DEBUG) && DEBUG
            NSSet *openSetCopy = FMDBReturnAutoreleased([[db valueForKey:@"_openResultSets"] copy]);
            for (NSValue *rsInWrappedInATastyValueMeal in openSetCopy) {
                FMResultSet *rs = (FMResultSet *)[rsInWrappedInATastyValueMeal pointerValue];
                NSLog(@"query: '%@'", [rs query]);
            }
#endif
        }
    });
    
    FMDBRelease(self);
}

注意:

  • 通过源码可知,FMDatabaseQueue提供了同步访问FMDatabase的方法,如果是在主线程操作FMDatabaseQueue话容易阻塞主线程,实际开发中,我们可以二度封装FMDatabaseQueue,提供异步访问FMDatabase的方法,只要将dispatch_sync改为dispatch_async就可以了。
  • FMDatabaseQueue利用了dispatch_queue_set_specific和dispatch_get_specific判断是不是当前queue,是因为防止创建了多个FMDatabaseQueue多线程同时访问数据库导致死锁

7.@synchronized

dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
    @synchronized(self) {
        sleep(2);
        NSLog(@"线程1");
    }
    sleep(1);
    NSLog(@"线程1解锁成功");
});
    
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
    @synchronized(self) {
        NSLog(@"线程2");
    }
});

// 线程1
// 线程2
// 线程1解锁成功
  • @synchronized(object)指令使用的object为该锁的唯一标识,只有当标识相同时,才满足互斥,所以如果线程2中的@synchronized(self)改为@synchronized(self.view),则线程2就不会被阻塞
  • @synchronized指令实现锁的优点就是我们不需要在代码中显示的创建锁对象,便可以实现锁的机制,但作为一种预防措施,@synchronized块会隐式的添加一个异常处理例程来保护代码,该处理例程会在异常抛出的时候自动的释放互斥锁。
  • 如果在@sychronized(object){}内部object被释放或被设为nil,从测试的结果来看,的确没有问题,但如果object被释放或被设为nil,从测试的结果来看,的确没有问题,但如果object一开始就是nil,则失去了锁的功能。但@synchronized([NSNull null])是完全可以的。

7.MLeaksFinder原理

利用isa-swizzling技术,将viewController的viewDidDisappearviewWillAppear以及dismissViewControllerAnimated:completion:进行交换,在viewDidDisappear会调用自定义的willDealloc方法,核心代码如下:

__weak id weakSelf = self;
    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
        __strong id strongSelf = weakSelf;
        [strongSelf assertNotDealloc];
    });

通过延后两秒,来判定viewController是否存在,存在则说明内存泄露,不存在则说明内存未泄露

8.FMDB数据库实现多读单写

FMDB数据库实现多读单写,只要在读取的时候将写的方法加锁即可。多读可以并发队列结合使用dispatch_semaphore信号量大于1控制并发数量来实现,加锁也可以使用dispatch_semaphore信号量从1降为0,解锁再升为1来实现。

9.HTTP的TCP协议和Socket的TCP协议有何不同?

HTTP的TCP协议是默认是短连接,TCP连接上,完成数据发送后即断开。Socket的TCP协议则是长连接,连接上后,只要不手动断开,则一直保持连接状态。

你可能感兴趣的:(iOS面试总结)