面试基础复习

1.数据结构的存储

数据结构的存储一般常用的有两种: 顺序存储结构 和 链式存储结构。
顺序存储结构和链式存储结构的区别:
链表存储结构的内存地址不一定是连续的,但顺序存储结构的内存地址一定是连续的;链式存储适用于在较频繁地插入、删除、更新元素时,而顺序存储结构适用于频繁查询时使用。
顺序存储结构和链式存储结构的优缺点:
1、空间上
顺序比链式节约空间。是因为链式结构每一个节点都有一个指针存储域。
2、存储操作上
顺序支持随机存取,方便操作
存取结构:分为随机存取和非随机存取(又称顺序存取)
  1)随机存取就是直接存取,可以通过下标直接访问的那种数据结构,与存储位置无关,例如数组。非随机存取
就是顺序存取了,不能通过下标访问了,只能按照存储顺序存取,与存储位置有关,例如链表。
  2)顺序存取就是存取第N个数据时,必须先访问前(N-1)个数据 (list),随机存取就是存取第N个数据时,
不需要访问前(N-1)个数据,直接就可以对第N个数据操作 (array)。
3、插入和删除上
链式的要比顺序的方便(因为插入的话顺序表也很方便,问题是顺序表的插入要执行更大的空间复杂度,包括一个从表头索引以及索引后的元素后移,而链表是索引后,插入就完成了)

例如:当你在字典中查询一个字母j的时候,你可以选择两种方式,第一,顺序查询,从第一页依次查找直到查询到j。第二,索引查询,从字典的索引中,直接查出j的页数,直接找页数,或许是比顺序查询最快的。
参考自:http://www.cnblogs.com/hi3254014978/p/9929955.html

2、什么是MVVM?使用MVVM的注意事项?使用MVVM的利弊?

1874977-0fb12f6848ba6e78.png
  • MVVM 的基本概念
    在MVVM 中,view 和 view controller正式联系在一起,我们把它们视为一个组件
    view 和 view controller 都不能直接引用model,而是引用视图模型(viewModel)
    viewModel 是一个放置用户输入验证逻辑,视图显示逻辑,发起网络请求和其他代码的地方
    使用MVVM会轻微的增加代码量,但总体上减少了代码的复杂性
  • MVVM 的注意事项
    view 引用viewModel ,但反过来不行(即不要在viewModel中引入#import UIKit.h,任何视图本身的引用都不应该放在viewModel中)(PS:基本要求,必须满足)
    viewModel 引用model,但反过来不行
  • MVVM 的使用建议
    • MVVM 可以兼容你当下使用的MVC架构。
    • MVVM 增加你的应用的可测试性。
    • MVVM 配合一个绑定机制效果最好(PS:ReactiveCocoa你值得拥有)。
    • viewController 尽量不涉及业务逻辑,让 viewModel 去做这些事情。
    • viewController 只是一个中间人,接收 view 的事件、调用 viewModel 的方法、响应 viewModel 的变化。
    • viewModel 绝对不能包含视图 view(UIKit.h),不然就跟 view 产生了耦合,不方便复用和测试。
    • viewModel之间可以有依赖。
    • viewModel避免过于臃肿,否则重蹈Controller的覆辙,变得难以维护。
  • MVVM 的优势
    低耦合:View 可以独立于Model变化和修改,一个 viewModel 可以绑定到不同的 View 上
    可重用性:可以把一些视图逻辑放在一个 viewModel里面,让很多 view 重用这段视图逻辑
    独立开发:开发人员可以专注于业务逻辑和数据的开发 viewModel,设计人员可以专注于页面设计
    可测试:通常界面是比较难于测试的,而 MVVM 模式可以针对 viewModel来进行测试
  • MVVM 的弊端
    数据绑定使得Bug 很难被调试。你看到界面异常了,有可能是你 View 的代码有 Bug,也可能是 Model 的代码有问题。数据绑定使得一个位置的 Bug 被快速传递到别的位置,要定位原始出问题的地方就变得不那么容易了。
    对于过大的项目,数据绑定和数据转化需要花费更多的内存(成本)。主要成本在于:
    数组内容的转化成本较高:数组里面每项都要转化成Item对象,如果Item对象中还有类似数组,就很头疼。
    转化之后的数据在大部分情况是不能直接被展示的,为了能够被展示,还需要第二次转化。
    只有在API返回的数据高度标准化时,这些对象原型(Item)的可复用程度才高,否则容易出现类型爆炸,提高维护成本。
    调试时通过对象原型查看数据内容不如直接通过NSDictionary/NSArray直观。
    同一API的数据被不同View展示时,难以控制数据转化的代码,它们有可能会散落在任何需要的地方。

3.GCD

GCD 中两个核心概念:
1.任务
放入GCD中的操作,一般以Block的方式进行,执行任务的操作有两种
a.同步执行:不会开启新的线程,在当前线程中执行,表现为同步函数sync
b.异步执行:拥有开启新线程执行任务的能力,变现为异步函数async
2.队列
任务队列,用来存放任务的队列.采用先进先出的原则,队列也分为两种
a.串行队列:串行队列中的任务一个接一个的执行,不会开启新的线程,即是在原来的串行队列开启的线程中顺序执行任务
b.并发队列:在异步函数中会开启多条线程,同时执行任务

 // 创建串行队列
    dispatch_queue_t serialQueue = dispatch_queue_create("mySerialQueue", DISPATCH_QUEUE_SERIAL);
    // 主队列,也为一个串行队列,主队列的任务一定在主线程中执行
    dispatch_queue_t mainQueue = dispatch_get_main_queue();
    // 创建并发队列,获取全局并发队列
    dispatch_queue_t concurrentQueue = dispatch_queue_create("myConcurrentQueue", DISPATCH_QUEUE_CONCURRENT);
    dispatch_queue_t globalQueue = dispatch_get_global_queue(0, 0);
    
    // 创建同步执行的任务
    // 同步函数+串行队列
    dispatch_sync(serialQueue, ^{
        // 不会开启新的线程
        NSLog(@"%@",[NSThread currentThread]);
    });
//    // 同步函数+主队列 程序会崩溃
//    dispatch_sync(mainQueue, ^{
//
//        NSLog(@"%@",[NSThread currentThread]);
//    });
    // 同步函数+并发队列
    dispatch_sync(concurrentQueue, ^{
        // 不会开启新的线程
        NSLog(@"%@",[NSThread currentThread]);
    });
    // 创建异步执行的任务
    // 异步函数+并发队列
    dispatch_async(concurrentQueue, ^{
        // 开启新的线程
        NSLog(@"异步函数+并发队列1%@",[NSThread currentThread]);
    });
    // 异步函数+并发队列
    dispatch_async(concurrentQueue, ^{
        // 开启新的线程
        NSLog(@"异步函数+并发队列2%@",[NSThread currentThread]);
    });
    // 异步函数+并发队列
    dispatch_async(concurrentQueue, ^{
        // 开启新的线程
        NSLog(@"异步函数+并发队列3%@",[NSThread currentThread]);
    });
    // 异步函数+串行队列
    dispatch_async(serialQueue, ^{
        // 开启一条子线程,串行执行任务
        NSLog(@"异步函数+串行队列1%@",[NSThread currentThread]);
    });
    // 异步函数+串行队列
    dispatch_async(serialQueue, ^{
        // 开启一条子线程,串行执行任务
        NSLog(@"异步函数+串行队列2%@",[NSThread currentThread]);
    });
    // 异步函数+串行队列
    dispatch_async(serialQueue, ^{
        // 开启一条子线程,串行执行任务
        NSLog(@"异步函数+串行队列3%@",[NSThread currentThread]);
    });
    // 异步函数+串行队列
    dispatch_async(serialQueue, ^{
        // 开启一条子线程,串行执行任务
        NSLog(@"异步函数+串行队列4%@",[NSThread currentThread]);
    });
    // 同步函数+并发队列
    dispatch_sync(concurrentQueue, ^{
        // 不会开启新的线程
        NSLog(@"同步函数+并发队列1%@",[NSThread currentThread]);
    });
    // 同步函数+并发队列
    dispatch_sync(concurrentQueue, ^{
        // 不会开启新的线程
        NSLog(@"同步函数+并发队列2%@",[NSThread currentThread]);
    });
    // 异步函数+主队列
    dispatch_async(mainQueue, ^{
        // 不开启新的线程
        NSLog(@"%@",[NSThread currentThread]);
    });
2.png

串行队列+异步函数

NSArray *titleArray = @[@"第一个任务",@"第二个任务",@"第三个任务"];
    dispatch_queue_t serialQueue = dispatch_queue_create("serialQueue""", DISPATCH_QUEUE_SERIAL);
    for (NSString *str in titleArray) {
        dispatch_async(serialQueue, ^{
            NSLog(@"串行队列+异步函数:%@--%@",str,[NSThread currentThread]);
        });
    }
    
    for (NSString *str in titleArray) {
        dispatch_async(dispatch_get_main_queue(), ^{
            NSLog(@"主队列+异步函数:%@--%@",str,[NSThread currentThread]);
        });
    }
86C0A0F47A0775BE27CC794AED11A1DB.jpg

可以发现,串行队列+异步函数的组合开启了新的线程,而主队列+异步函数确没有开启新的线程,仍在主线程执行任务.
并发队列+同步函数

NSArray *titleArray = @[@"第一个任务",@"第二个任务",@"第三个任务",@"第四个任务",@"第五个任务"];
    dispatch_queue_t concurrentQueue = dispatch_queue_create("concurrentQueue""", DISPATCH_QUEUE_CONCURRENT);
    for (NSString *str in titleArray) {
        dispatch_sync(concurrentQueue, ^{
            NSLog(@"并发队列+同步函数:%@--%@",str,[NSThread currentThread]);
        });
    }
    
    for (NSString *str in titleArray) {
        dispatch_sync(dispatch_get_global_queue(0, 0), ^{
            NSLog(@"全局并发队列+同步函数:%@--%@",str,[NSThread currentThread]);
        });
    }
image.png

证实了并行队列在同步执行中并不会开辟新的线程,所有的任务都是在主线程中完成,并且任务为一个一个的串行执行。

4.线程间通信

1 .一个线程传递数据给另一个线程
2 .在一个线程中执行完特定任务后,转到另一个线程继续执行任务
线程间通信常用的方法

  1. NSThread可以先将自己的当前线程对象注册到某个全局的对象中去,这样相互之间就可以获取对方的线程对象,然后就可以使用下面的方法进行线程间的通信了,由于主线程比较特殊,所以框架直接提供了在主线程执行的方法
- (void)performSelectorOnMainThread:(SEL)aSelector withObject:(nullable id)arg waitUntilDone:(BOOL)wait;

- (void)performSelector:(SEL)aSelector onThread:(NSThread *)thr withObject:(nullable id)arg waitUntilDone:(BOOL)wait NS_AVAILABLE(10_5, 2_0);
用法如下:

//点击屏幕开始执行下载方法
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
    [self performSelectorInBackground:@selector(download) withObject:nil];
}

//下载图片
- (void)download
{    
    // 1.图片地址
    NSString *urlStr = @"http://d.jpg"; 
    NSURL *url = [NSURL URLWithString:urlStr];
    // 2.根据地址下载图片的二进制数据
    NSData *data = [NSData dataWithContentsOfURL:url];
    NSLog(@"---end");
    // 3.设置图片
    UIImage *image = [UIImage imageWithData:data];
    // 4.回到主线程,刷新UI界面(为了线程安全)
    [self performSelectorOnMainThread:@selector(downloadFinished:) withObject:image waitUntilDone:NO];
   // [self performSelector:@selector(downloadFinished:) onThread:[NSThread mainThread] withObject:image waitUntilDone:YES];
}

- (void)downloadFinished:(UIImage *)image
{
    self.imageView.image = image;
    NSLog(@"downloadFinished---%@", [NSThread currentThread]);
}
  1. GCD一个线程传递数据给另一个线程,如:
  • (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
    { dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
    NSLog(@"donwload---%@", [NSThread currentThread]);
    // 1.子线程下载图片
    NSURL *url = [NSURL URLWithString:@"http://d.jpg"];
    NSData *data = [NSData dataWithContentsOfURL:url];
    UIImage *image = [UIImage imageWithData:data];
    // 2.回到主线程设置图片
    dispatch_async(dispatch_get_main_queue(), ^{
    NSLog(@"setting---%@ %@", [NSThread currentThread], image);
    [self.button setImage:image forState:UIControlStateNormal];
    });
    });
    }
    3、NSOperation、NSOperationQueue 线程间的通信
/**
 * 线程间通信
 */
- (void)communication {

    // 1.创建队列
    NSOperationQueue *queue = [[NSOperationQueue alloc]init];

    // 2.添加操作
    [queue addOperationWithBlock:^{
        // 异步进行耗时操作
        for (int i = 0; i < 2; i++) {
            [NSThread sleepForTimeInterval:2]; // 模拟耗时操作
            NSLog(@"1---%@", [NSThread currentThread]); // 打印当前线程
        }

        // 回到主线程
        [[NSOperationQueue mainQueue] addOperationWithBlock:^{
            // 进行一些 UI 刷新等操作
            for (int i = 0; i < 2; i++) {
                [NSThread sleepForTimeInterval:2]; // 模拟耗时操作
                NSLog(@"2---%@", [NSThread currentThread]); // 打印当前线程
            }
        }];
    }];
}

通过线程间的通信,先在其他线程中执行操作,等操作执行完了之后再回到主线程执行主线程的相应操作。

5.创建常驻线程

线程常驻,正如其名,我们要实现的是让一个线程长期存在,不被销毁。
网上有些地方写的常驻线程存在内存泄漏,下面贴上更改好的
1.设置成全局的,如果是线程对象是局部的就会死掉
2.初始化线程并启动
3.启动RunLoop,子线程的RunLoop默认是停止的,需要手动开启

@interface NNNineViewController ()

@property (nonatomic, strong) NSThread *thread;
@property (nonatomic, assign) BOOL runloopLife;//用于控制内存泄漏
@property (nonatomic, strong) NSTimer *timer;
@end

@implementation NNNineViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    self.view.backgroundColor = [UIColor whiteColor];
    self.runloopLife = YES;
    self.thread = [[NSThread alloc]initWithTarget:self selector:@selector(test) object:nil];
    [self.thread start];
}

- (void)viewWillDisappear:(BOOL)animated
{
    [super viewWillDisappear:animated];
    self.runloopLife = NO;
    [self.timer invalidate];
    self.timer = nil;
    NSLog(@"viewWillDisappear");
}

- (void)test{
    //方式1、添加Port 实时监听
//    [[NSRunLoop currentRunLoop] addPort:[NSPort port] forMode:NSDefaultRunLoopMode];
    
    //方式2、添加一个定时器
    NSTimer *timer = [NSTimer timerWithTimeInterval:10.0 target:self selector:@selector(print) userInfo:nil repeats:YES];
    self.timer = timer;
    [[NSRunLoop currentRunLoop] addTimer:timer forMode:NSDefaultRunLoopMode];
    
    //添加runloop,如果没有输入源或定时器连接到运行循环,则此方法立即退出; 否则,它通过重复调用runMode:beforeDate:来在NSDefaultRunLoopMode中运行接收器。 换句话说,这种方法有效地开始一个无限循环,用于处理来自运行循环的输入源和定时器的数据。
//    [[NSRunLoop currentRunLoop]run];
    while (self.runloopLife) {
        //运行循环一次,阻塞在指定模式下,直到给定的日期到达。如果没有输入源连接到运行循环,此方法立即退出,并返回NO,否则,在处理第一个输入源或达到limitDate后返回YES,如果手动从运行循环中删除所有已知的输入源和定时器并不能保证运行循环会立即退出
        BOOL b = [[NSRunLoop currentRunLoop]runMode:NSDefaultRunLoopMode beforeDate:[NSDate dateWithTimeIntervalSinceNow:0.3]];//会阻塞6s
        NSLog(@"b = %@", b == YES ? @"YES" : @"NO");
    }
    
}

- (void)run{
    
    NSLog(@"%@ %s",[NSThread currentThread],__func__);
}

- (void)print {
    NSLog(@"timer打印。。。。。");
}

- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
    [self performSelector:@selector(run) onThread:self.thread withObject:nil waitUntilDone:NO];
}

- (void)dealloc {
    NSLog(@"常驻线程控制器销毁。。。。。");
}
@end

6.深浅复制

非集合类对象中

· [immutableObject copy] // 浅复制
· [immutableObject mutableCopy] //深复制
· [mutableObject copy] //深复制
· [mutableObject mutableCopy] //深复制

集合类对象中

· [immutableObject copy] // 浅复制
· [immutableObject mutableCopy] //单层深复制
· [mutableObject copy] //单层深复制
· [mutableObject mutableCopy] //单层深复制

不管是集合类对象还是非集合类对象,接收到copy和mutableCopy消息时,都遵循下面规则:
copy返回immutable对象,所以对copy返回值使用mutable对象接口会崩溃;
mutableCopy返回mutable对象。

KVO的本质?

键值监听。监听一个对象的属性的任何变化来做出相应的响应。
KVO中有两个关键的方法。

/***************
      添加观测者
     @observer:就是观察者,是谁想要观测对象的值的改变。
     @keyPath:就是想要观察的对象属性。
     @options:options一般选择NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld,这样当属性值发生改变时我们可以同时获得旧值和新值,如果我们只填NSKeyValueObservingOptionNew则属性发生改变时只会获得新值。
     @context:想要携带的其他信息,比如一个字符串或者字典什么的。
     **************/
    - (void)addObserver:(NSObject *)observer forKeyPath:(NSString *)keyPath options:(NSKeyValueObservingOptions)options context:(nullable void *)context;
 /********************
    当所观测的属性值发生改变时调用的函数
     @keyPath:观察的属性
     @object:观察的是哪个对象的属性
     @change:这是一个字典类型的值,通过键值对显示新的属性值和旧的属性值
     @context:上面添加观察者时携带的信息
     *******************/
    - (void)observeValueForKeyPath:(nullable NSString *)keyPath ofObject:(nullable id)object change:(nullable NSDictionary *)change context:(nullable void *)context;

当监听Person对象的name属性值得变化后,有下面的


image.png
KVO内部调用顺序
  • 首先调用willChangeValueForKey:方法。
  • 然后调用setAge:方法真正的改变属性的值。
  • 开始调用didChangeValueForKey:这个方法,调用[super didChangeValueForKey:key]时会通知监听者属性值已经改变,然后监听者执行自己的- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context这个方法。
    参考地址:https://www.jianshu.com/p/d0032fd9397b
直接修改成员变量会触发KVO吗?

不会,KVO的本质是set方法,只有调用了set方法才会触发KVO。

KVO可以监听可变数组的变化吗?

KVO监听的是地址变化,可变数组里加了个元素,这是数组的内容变了,但是地址没有变,所以不会调用KVO
比如直接监听当前ViewController中的数组不管是count还是lastObject等都会导致崩溃 ,所以需要一些其他操作来达到目的。
count会直接报错,NSMutableArray 没有 count的 keyPath。
*** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '[<__NSArrayM 0x282195770> addObserver:forKeyPath:options:context:] is not supported. Key path: count'
使用常规的KVO方法对NSMutableArray进行监控,只有当整个array对象被改变时(如直接设置array1 = array2)时才能触发KVO,无法监控到array的增、删、改操作。
如果希望通过KVO监控到数组对象的增、删、改操作,需要通过以下步骤:
1. 将NSMutableArray对象作为属性封装到一个类中;
2. 在需要进行KVO的类中对该属性add observer
3. 最重要的一点:使用mutableArrayValueForKey:方法获取NSMutableArray属性,并通过其进行修改。
参考:https://www.jianshu.com/p/75f7b27a3e48

手动触发KVO

我们关闭了-willChangeValueForKey:和 -didChangeValueForKey:的自动调用,然后我们手动调用他们。我们只应该在关闭了自动调用的时候我们才需要在 setter 方法里手动调用 -willChangeValueForKey:和-didChangeValueForKey:,否则会调用两次observeValueForKeyPath:ofObject:change:context:方法。
参考地址:https://developer.apple.com/library/archive/documentation/Cocoa/Conceptual/KeyValueObserving/Articles/KVOCompliance.html#//apple_ref/doc/uid/20002178-SW3

#import "ViewController.h"

@interface ViewController ()
@property (nonatomic, strong) NSString *name;
@end

@implementation ViewController

+(BOOL)automaticallyNotifiesObserversForKey:(NSString *)key{
    if ([key isEqualToString:@"name"]) {
        return NO;
    }else{
        return [super automaticallyNotifiesObserversForKey:key];
    }
}

-(void)setName:(NSString *)name{

    if (_name != name) {
        NSLog(@"1");
        [self willChangeValueForKey:@"name"];
        NSLog(@"2");
        _name = name;
        NSLog(@"3");
        [self didChangeValueForKey:@"name"];
        NSLog(@"4");
    }
}

- (void)viewDidLoad {
    [super viewDidLoad];
    self.name = @"AAAAA";
    [self addObserver:self forKeyPath:@"name" options:NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld context:nil];
}

- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
    [super touchesBegan:touches withEvent:event];

    NSLog(@"点击屏幕。。。。。");
    self.name = @"dddd";
}

- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context {
    if ([keyPath isEqualToString:@"name"]) {
        NSLog(@"hahahahahaha........");
    }
}
@end

打印结果


image.png

8.响应者链

image.png

9.NSString用copy还是strong修饰?

  • 当我们十分确定,要给属性NSString赋一个不可变的值时,用strong。如果使用copy来修饰属性,在进行赋值的时候,会先做一个类型判断,如果赋的值是一个不可变的字符串,则走strong的策略,进行的是浅拷贝;如果是可变的字符串,则进行深拷贝创建一个新的对象。所以如果我们确定是给属性赋值一个不可变的值,就不用copy再多去判断一遍类型,因为如果是很多的NSString属性需要赋值,会极大的增加系统开销。所以用strong在此情况下可以提升性能。
  • 但是很多情况下我们并不能确定要赋的值是什么类型的,所以我们还是使用copy来修饰,这样保证了安全性。因为如果赋值的可变的字符串,当它发生变化时,用strong修饰的属性的值也会跟着变化;而copy修饰的属性,则因为是深拷贝而不会变化。
    参考地址:https://www.jianshu.com/p/ddfd5cc0c1b4

10.assign和weak的区别

在MRC中是没有weak的概念直到ARC中才有weak的出现,而assign是一直存在,两朝元老:
共同点:

  • 不是强引用, 不能保住OC对象的生命
  • 都可以作用于对象
    不同点
  • 在MRC中是没有weak的概念直到ARC中才有weak的出现,而assign是一直存在,两朝元老
  • weak只能作用于对象。如果修饰基本数据类型,编译器会报错“Property with ‘weak’ attribute must be of object type”。
  • assign不但能修饰对象还能修饰基本数据类型。“能”修饰基本数据类型仅表示可以编译成功。当需要修饰对象类型时,MRC时代使用unsafe_unretained。当然,unsafe_unretained也可能产生野指针,所以它名字是"unsafe_”。
  • weak所指向的对象销毁时会将当前指向对象的指针指向nil,防止野指针的生成
  • assign所指向的对象销毁时不会将当前指向对象的指针指向nil,有野指针的生成
    所以在ARC中一般数据类型指定为assign而对象则会指定为weak、strong、copy。
    总结
  • assign 适用于基本数据类型如int,float,struct等值类型,不适用于引用类型。因为值类型会被放入栈中,遵循先进后出原则,由系统负责管理栈内存。而引用类型会被放入堆中,需要我们自己手动管理内存或通过ARC管理。
    weak 适用于delegate和block等引用类型,不会导致野指针问题,也不会循环引用,非常安全。

11.atomic 和 nonatomic 有什么区别?

  • Atomic
    是默认的
    会保证 CPU 能在别的线程来访问这个属性之前,先执行完当前流程
    速度不快,因为要保证操作整体完成
    atomic只是保证了getter和setter存取方法的线程安全,并不能保证整个对象是线程安全的
  • NonAtomic
    不是默认的
    更快
    线程不安全
    如有两个线程访问同一个属性,会出现无法预料的结果
    参考:
    https://www.jianshu.com/p/b058e5ea2cad
    https://www.jianshu.com/p/7288eacbb1a2

13.__block和__weak修饰符的区别其实是挺明显的:

1.__block不管是ARC还是MRC模式下都可以使用,可以修饰对象,还可以修饰基本数据类型。
2.__weak只能在ARC模式下使用,也只能修饰对象(NSString),不能修饰基本数据类型(int)。
3.__block对象可以在block中被重新赋值,__weak不可以。
4.__block对象在ARC下可能会导致循环引用,非ARC下会避免循环引用,__weak只在ARC下使用,可以避免循环引用。

在硬盘查找图片的键值是什么

https://my.oschina.net/ososchina/blog/1604020
1.入口 setImageWithURL:placeholderImage:options:会先把 placeholderImage显示,然后 SDWebImageManager根据 URL 开始处理图片。

2.进入SDWebImageManager 类中downloadWithURL:delegate:options:userInfo:,交给
SDImageCache从缓存查找图片是否已经下载
queryDiskCacheForKey:delegate:userInfo:.

3.先从内存图片缓存查找是否有图片,如果内存中已经有图片缓存,SDImageCacheDelegate回调 imageCache:didFindImage:forKey:userInfo:到
SDWebImageManager。

4.SDWebImageManagerDelegate 回调
webImageManager:didFinishWithImage: 到 UIImageView+WebCache,等前端展示图片。

5.如果内存缓存中没有,生成 `NSOperation `
添加到队列,开始从硬盘查找图片是否已经缓存。

6.根据 URL的MD5值Key在硬盘缓存目录下尝试读取图片文件。这一步是在 NSOperation 进行的操作,所以回主线程进行结果回调 notifyDelegate:。

7.如果上一操作从硬盘读取到了图片,将图片添加到内存缓存中(如果空闲内存过小, 会先清空内存缓存)。SDImageCacheDelegate'回调 imageCache:didFindImage:forKey:userInfo:`。进而回调展示图片。

8.如果从硬盘缓存目录读取不到图片,说明所有缓存都不存在该图片,需要下载图片, 回调 imageCache:didNotFindImageForKey:userInfo:。

9.共享或重新生成一个下载器 SDWebImageDownloader开始下载图片。

10.图片下载由 NSURLConnection来做,实现相关 delegate
来判断图片下载中、下载完成和下载失败。

11.connection:didReceiveData: 中利用 ImageIO做了按图片下载进度加载效果。

12.connectionDidFinishLoading: 数据下载完成后交给 SDWebImageDecoder做图片解码处理。

13.图片解码处理在一个 NSOperationQueue完成,不会拖慢主线程 UI.如果有需要 对下载的图片进行二次处理,最好也在这里完成,效率会好很多。

14.在主线程 notifyDelegateOnMainThreadWithInfo:
宣告解码完成 imageDecoder:didFinishDecodingImage:userInfo: 回调给 SDWebImageDownloader`。

15.imageDownloader:didFinishWithImage:回调给 SDWebImageManager告知图片 下载完成。
-16. 通知所有的 downloadDelegates下载完成,回调给需要的地方展示图片。

17.将图片保存到 SDImageCache中,内存缓存和硬盘缓存同时保存。写文件到硬盘 也在以单独 NSOperation 完成,避免拖慢主线程。

18.SDImageCache 在初始化的时候会注册一些消息通知,
在内存警告或退到后台的时 候清理内存图片缓存,应用结束的时候清理过期图片。

SDWebImageDecoder的作用

https://blog.csdn.net/lidongxuedecsdn/article/details/79868135
https://www.jianshu.com/p/7dea5b081d24?utm_campaign=hugo&utm_medium=reader_share&utm_content=note&utm_source=qq
为什么要对图片进行解码?其实我们自己不解码图片我们也是可以直接使用的(其实是系统为我们进行了解码的操作),一般下载的图片或者我们手动拖进主bundle 的图片都是PNG 或者JPG 其他格式的图片,这些图片都是经过编码压缩后的图片数据,并不是控件可以直接显示的位图,如果我们直接使用 "+ (nullable UIImage *)imageNamed:(NSString *)name" 来加载图片,系统默认会在主线程立即进行图片的解码工作,这个过程就是把图片数据解码成可供控件直接显示的位图数据,由于这个解码操作比较耗时,并且默认是在主线程进行,所以当在主线程调用了大量的 "+ (nullable UIImage *)imageNamed:(NSString *)name" 方法后就会产生卡顿。(同时由于位图体积较大,所以在磁盘缓存中不会直接缓存位图数据,而是编码压缩过的PNG 活着JPG 数据)

推送原理

  • 由App向iOS设备发送一个注册通知
  • iOS向APNs远程推送服务器发送App的Bundle Id和设备的UDID
  • APNs根据设备的UDID和App的Bundle Id生成deviceToken再发回给App
  • App再将deviceToken发送给远程推送服务器(商家自己的服务器), 由服务器保存在数据库中
  • 当商家想发送推送时, 在远程推送服务器中输入要发送的消息并选择发给哪些用户的deviceToken,由远程推送服务器发送给APNs
  • APNs根据deviceToken发送给对应的用户
    (1) APNs 服务器就是苹果专门做远程推送的服务器.
    (2)deviceToken是由APNs生成的一个专门找到你某个手机上的App的一个标识码.
    (3) deviceToken 可能会变,如果你更改了你项目的bundle Identifier或者APNs服务器更新了可能会变.

iOS面试之__HTTP、Socket、TCP的区别

https://www.jianshu.com/p/3a869b0b095c

iOS scheme跳转机制

https://www.jianshu.com/p/138b44833cda

你可能感兴趣的:(面试基础复习)