基础知识整理

123 阅读11分钟

weak是如何实现自动置nil的

总结:当一个对象obj被weak指针指向时,这个weak指针会以obj作为key,被存储到sideTable类的weak_table这个散列表上对应的一个weak指针数组里面。 当一个对象obj的dealloc方法被调用时,Runtime会以obj为key,从sideTable的weak_table散列表中,找出对应的weak指针列表,然后将里面的weak指针逐个置为nil runtime维护了一个弱引用表, 用于储存指向某个对象的所有weak指针, weak表其实是一个hash表, key是所指对象的地址, value是weak指针的地址数组

  1. 初始化: runtime调用objc_initweak函数,初始化一个新的weak指针指向对象的地址
  2. 添加引用时: objc_initweak函数会调用objc_storeweak函数, objc_storeweak的作用是更新指针指向,创建对应的弱引用表
  3. 释放时: 调用clearDeallocating函数. clearDeallocating函数会根据对象地址获取weak指针地址的数组, 遍历这个数组,把其中的数据设为nil, 最后把entry从weak表中删除, 清理对象的记录

assign修饰对象会出现什么情况

对象分配在堆上, 堆中的内存是程序员自己控制的, 当内存被释放后, 指针的地址还是存在的, 会造成野指针, 当后续内存分配到这块内存,会造成崩溃

assgin修饰对象,对象释放后,为什么指针地址还在

ARC下对象销毁,会将weak引用表中的指针逐个置nil, 对于assign没有这个操作

strong修饰对象,对象释放为什么指针地址会自动置为nil

strong修饰对象是引用计数加一,就是进行了retain的操作, 对象的释放是因为没有强引用了,引用计数为0, release操作中把指针置为nil了

strong和retain的区别 修饰OC对象的时候, strong和retain是没有什么区别的, 修饰Block的时候, strong相当于copy, retain相当于assign, 用retain修饰block, block没有进行copy, 类型为栈block, 栈中的内存由系统管理, 随时有可能销毁掉, 所以用retain修饰block的话, 可能会出现崩溃, 也有可能输出错误的值

static和const的区别 const是只读的意思, 不可改变 static是规定作用域和存储方式

修饰局部变量 1、延长局部变量的生命周期,程序结束才会销毁 2、局部变量只会生成一份内存,只会初始化一次

修饰全局变量 1、只能在本文件中访问,修改全局变量的作用域,生命周期不会改 2、避免重复定义全局变量

OC内存分区和分配

  1. 栈区:栈区存放定义的基本数据类型, 如int, double等, 函数的执行也在栈区, 传入函数体的参数, 函数体内声明的局部变量, 都在函数体执行结束后自动释放
  2. 堆区:是有代码分配和释放的, 用于存放进程运行中被动态分配的内存段,堆区的内存是动态分配的,调用alloc的函数时分配内存

- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view, typically from a nib.
    
    int flag;//flag存放在<栈区>
    NSString *number = @"8090";//number存放在<栈区>,@"8090"存放在<常量区>
    
    //array_1、array_2存放在<堆区>
    NSArray *array_1 = [[NSArray alloc]init];
    NSArray *array_2 = [NSArray array];
    
    NSArray *array_3 = @[];//array_3存放在<栈区>
    
    NSInteger total = [self getTotalNumber:1 number2:1];//total存放在<栈区>
    
}
 
- (NSInteger)getTotalNumber:(NSInteger)number1 number2:(NSInteger)number2{
    return number1 + number2;//number1、number2存放在<栈区>
}
  1. 全局/静态区: 存放全局变量和静态变量的, 全局/静态区的内存在编译阶段完成分配, 程序运行时一直存在在内存中, 程序结束后才会由操作系统释放,(已初始化的全局变量和静态变量存放在一块区域,未初始化的全局变量和静态变量存放在相邻的另一块区域)
NSInteger age;//age存放在<未初始化的全局静态区>
 
NSInteger  score = 100;//age存放在<已初始化的全局静态区>
 
//@"SunSatan"存放在<常量区>,name存放在<已初始化的全局静态区>
NSString *name = @"SunSatan";
 
static NSString *titleView = @"SunSatan";//titleView存放在<已初始化的全局静态区>
  1. 常量区: 存放常量,常量字符串存放在常量区, 在编译完成分配, 一直在内存中, 程序结束后由操作系统释放
  2. 代码区:存放程序的所有代码

为什么函数执行时是在栈中的

OC的内存管理

1.内存管理管理的是堆上的内存,栈中的内存是由系统管理的,内存管理的原理就是引用计数器, 当对象的引用计数为0时, 就将被销毁, 占用的内存也会被系统回收, oc会自动给对象发送一条dealloc消息, retain是进行了Objc_retain的操作, release是进行了Objc_release的操作

2.RAC环境下出现的内存问题主要是循环引用造成的, 比如Block,代理等, 使用weak这种弱引用的方式来打破循环引用的问题, 为什么weak就可以打破循环引用参看weak修饰符自动置nil的问题

3.检测内存泄漏的工具有xcode自带的Instrument中的leaks来检测, 也可以使用三方工具如MLeaksFinder, 其原理是为基类NSObject添加一个方法, willDealloc, 并用一个弱指针指向self, 在一段时间之后, 调用assertNotDealloc方法, 如果已经释放了向nil发消息是不会进入assertNotDealloc方法的, 如果进入的这个方法就说明内存没有释放, 可能造成了内存泄漏了

4.Core Founddation是基于C语言的接口, 所以RAC不支持对其对象的管理, 所以需要自己手动管理, 否则会造成内存泄漏

属性和成员变量的区别

属性可以自动生成set和get方法, 并创建一个带下划线的成员变量,属性可以用点语法调用, 点语法实际上是调用的set和get方法, 成员变量不会自动生成set和get方法, 不能用点语法调用,只能使用->调用

为什么分类中不能添加属性

1.首先分类的结构体中没有objc_ivar_list这个属性列表 2.添加的属性, 不会自动生成带下划线的成员变量, 会添加set和get方法但是没有实现 3.没有实现set和get方法,所以不能访问

怎么在分类中添加属性

因为添加的属性没有实现set和get方法, 使用runtime提供的关联方法来实现set和get方法, 从而把属性绑定到原类中, 只能增加属性,不能增加成员变量

为什么分类要如此设计

程序启动时候是分类的加载是在原类之后的, 类加载后内存分布已经确定了, 如果分类添加属性就可以改变原类的内存, 会出现很多问题, 所以只能通过运行时的方法把属性关联到原类中, 运行时的关联操作把属性关联到原类中

AutoreleasePool

自动释放池, 是在超出释放池的生命周期后, 对其中的对象实例发送release的操作,就是延时调用release方法 在ARC环境中,用到自动释放池的地方不多, 例如短时间内创建大量的临时变量,造成内存峰值, 就需要手动添加自动释放池来释放临时变量,如编写的不是基于UI框架的程序, 例如命令行工具 NSThread和NSOperationQueue开辟子线程需要手动创建autoreleasepool,GCD开辟子线程不需要手动创建autoreleasepool,因为GCD的每个队列都会自行创建autoreleasepool

KVO相关问题

  1. KVO是键值观察, 注册观察者, 实现回调方法, 移除观察者

  2. KVO只能用于监听对象属性的变化,

通知,代理,block的使用场景,有什么不同

代理和block都是一对一的 通知是一对多,多对一的 使用通知的情况是在不确定什么时候会发生的时候使用通知传值

通知的发送和接收必须在一个线程里么, 如何做到在一个线程发送在另一个线程接收

不管你在哪个线程注册通知, 发送通知在哪个线程,接收通知就在哪个线程,接收和发送在同一个线程

接收通知处理代码线程 由发出通知的线程决定 发通知(主线程)-监听通知(子线程):接收通知代码在主线程处理 发通知(子线程)-监听通知(主线程):接收通知代码在子线程处理

- (void)viewDidLoad {
    [super viewDidLoad];
    [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(notificationCode) name:@"noti" object:nil];
}

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        [[NSNotificationCenter defaultCenter] postNotificationName:@"noti" object:nil];
    });
}
/** 监听通知会调用方法
 * 接收通知处理代码线程 由发出通知的线程决定
 * 发通知(主线程)-监听通知(子线程):接收通知代码在主线程处理
 * 发通知(子线程)-监听通知(主线程):接收通知代码在子线程处理
 */
- (void)notificationCode{
    dispatch_async(dispatch_get_main_queue(), ^{
       //更新 UI 操作
    });
    NSLog(@"notificationCode:%@",[NSThread currentThread]);
}
- (void)dealloc{
    [[NSNotificationCenter defaultCenter] removeObserver:self];
}

接收通知处理代码线程 由NSNotificationCenter的队列决定 发通知(主线程)-监听通知(并发队列):接收通知代码在子线程处理 发通知(子线程)-监听通知(主队列):接收通知代码在主线程处理

@property (nonatomic,weak) id observe;
- (void)viewDidLoad {
    [super viewDidLoad];
    _observe = [[NSNotificationCenter defaultCenter] addObserverForName:@"noti" object:nil queue:nil usingBlock:^(NSNotification * _Nonnull note) {
        /**处理通知代码**/
        NSLog(@"usingBlock:%@",[NSThread currentThread]);
    }];
}
- (void)dealloc{
    [[NSNotificationCenter defaultCenter] removeObserver:_observe];
}
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        [[NSNotificationCenter defaultCenter] postNotificationName:@"noti" object:nil];
    });
}

设置队列

_observe = [[NSNotificationCenter defaultCenter] addObserverForName:@"noti" object:nil queue:[NSOperationQueue mainQueue] usingBlock:^(NSNotification * _Nonnull note) {
        /**处理通知代码**/
        NSLog(@"usingBlock:%@",[NSThread currentThread]);
    }];

block会产生循环引用的问题,为什么

uiview动画的block中会产生循环引用的问题么, 为什么

uiview动画的block不会造成循环引用的原因是:这是个类方法, 当前控制器不可能强引用一个类, 所以循环引用无法形成

iOS动画有几种

  1. 隐式动画: uiview animation改变bounds, frame, transform等属性
  2. basic动画: CABasicAnimation创建, 设置keyPath及属性, 最终值, 时间, 重复次数等, 添加到layer上
  3. 关键帧动画:CAKeyFrameAnimation创建, 一种为设置路径动画, 即用beziper划线, 一种用values动画, 添加值

为什么向nil发消息不会崩溃

OC中函数调用是通过objc_msgSend()发送消息实现的, objc_msgSend()有参数, 第一个就是id类型的参数, 给nil发消息, 方法会判断参数id是否为nil, 如果为nil则SEL参数就会被置空,直接返回 如果返回值是对象, 给nil发消息返回0 如果方法返回是指针类型, 指针类型的大小是小于等于 sizeof(void *), float, double, long double, long long, 给nil发消息返回0 如果返回值是结构体, 会返回的结构体中各个字段都为0, 其他结构体数据类型的就不会用0填充 如果返回值不是上述几种情况, 则会返回undefined

程序启动流程

程序是如何加载动态库的

动态库和静态库有什么区别

多线程信号量

信号量 create, signal, wait 三个方法, 当信号量为<= 0时, wait会一直等待, 当信号量>0时会正常执行, 信号量减一, signal是增加一个信号量, wait相当于加锁, signal相当于解锁

load方法加载时机

1.load加载是在main函数之前, 由程序自己调用, 根据函数地址直接调用 2.调用时刻,是runtime在加载类,分类时 3.先加载父类的load方法,再加载子类的load方法 4.分类的加载没有顺序, 执行顺序是与其在complie source顺序一致 5.不同类的load的执行顺序也与编译源的顺序一致

initialize方法加载

1.在类或子类第一次收到消息前调用 2.子类没有实现initialize时候, 会调用父类的initialize, 当子类实现initialize方法,会覆盖父类的initialize方法 3. 分类中实现initialize, 会覆盖类中的方法, 只执行一个, complie source列表中最后一个分类的initialize方法

什么是隐式动画和显示动画

使用过什么设计模式

  1. 单例模式
  2. 观察者模式: KVO和通知实现原理都是观察者模式, 都是用于监听
  3. 工厂模式

比较原生、RN、Weex、Flutter

RN是一种桥接, 桥接了系统服务和系统UI, 性能可能会有一些问题 Flutter是Dart语言开发, 可以被编译成不同平台的本地代码不需要桥接, 提高了性能 Weex基础跟RN基本一致, 使用JS语言开发