OC中对象的结构
Instance对象
如果是NSObject对象,对象中只有一个isa指针,在64位中占16个字节(可以通过malloc_size函数获得),但实际只用到了8个字节(可以通过class_getInstanceSize函数获得)
这个isa指针指向了class类对象,通过isa指针&isa_mask可以获得NSObject类的地址
如果自定义的类,如果增加了属性或成员变量,对象中会有成员变量值得存储地址
对象方法、属性、成员变量、协议信息存放在类对象中,类方法存放在元类对象(可以通过get_class获得)中
meta-class对象
isa指针指向了基类的meta-class对象,superClass指针指向了父类的meta-class对象。基类的meta-class对象的superClass指针指向了基类的class对象
结构与class对象一致,只是只存储了类方法信息
isa指针
在arm64架构之前isa指针直接存储了class、meta-class对象的地址
在arm64之后isa指针经过优化,使用了公用体(union)结构

assing可以使用在对象中吗
可以,但会造成坏内存访问的错误

weak如何实现自动赋nil
runtime 对注册的类, 会进行布局,对于 weak 对象会放入一个 hash 表中。 用 weak 指向的对象内存地址作为 key,当此对象的引用计数为0的时候会 dealloc, 在这个 weak 表中搜索,找到所有以key为键的 weak 对象,从而设置为 nil

可变数组的实现原理
NSArrayM 用了环形缓冲区 (circular buffer),环形缓冲区有一些非常酷的属性。尤其是,除非缓冲区满了,否则在任意一端插入或删除均不会要求移动任何内存。我们来分析这个类如何充分利用环形缓冲区来使得自身比 C 数组强大得多。在任意一端插入或者删除,只是修改offset参数,不需要移动内存,我们访问的时候只是不和普通的数组一样index多少就是多少,这里会计算加上offset之后处理的值取数据,而不是插入头和尾巴的时候,环形结构会根据最少移动内存指针的方式插入,例如要在A和B之间插入,按照C的数组,我们需要把B到E的元素移动内存,但是环形缓冲区的设计,我们只要把A的值向前移动一个单位内存,即可,同时修改offset偏移量,就能保证最小的移动单元来完成中间插入。
往中部插入对象有非常相似的结果。合理的解释就是,
NSArrayM 试着去最小化内存的移动,因此会移动最少的一边元素。
NSMutableArray 是一个高级抽象数组,解决了 C 风格数组对应的缺点。(C数组插入的时候都会移动内存,不是O(1),用到了环形缓冲区数据结构来处理内存移动的损耗)
但是可变数组任意一端插入或删除能有固定时间的性能。而且在中间插入和删除的时候都会试着去移动最小化内存。
环形缓冲区的数据结构如果是连续数组结构,在扩容的时候难免会移动大量内存,因此用链表实现环形缓冲会更好

Block的循环引用、如何解决、原理
block内部也有isa指针
三种block:globalBlock、stackBlock、mallocBlock
block在修饰auto变量是值传递,在修饰静态变量是指针传递,在修饰全局变量不需要传递参数
如果block被拷贝到堆上,内部会调用_block_object_assign,如果auto对象是被strong或copy修饰会形成强引用
可以用weak或unsafe_unretained

layoutIfNeeded和setNeedsLayout的区别

  • setNeedsLayout

会标记为需要重新布局,异步调用layoutIfNeeded刷新布局,但不是立刻执行,是在RunLoop下一轮循环结束前刷新。对于这一轮runloop之内的所有布局和UI上的更新只会刷新一次,layoutSubviews一定会被调用

  • layoutIfNeeded

如果有需要刷新标记,会立刻调用layoutSubviews进行刷新操作,如果没有则不调用

两个对象isEquel相等,hash一定相等,但hash相等,isEque不一定相等

进程间通信的方式
管道pipe:管道是一种半双工的通信方式,数据只能单向流动,而且只能在具有亲缘关系的进程间使用。进程的亲缘关系通常是指父子进程关系。
命名管道FIFO:有名管道也是半双工的通信方式,但是它允许无亲缘关系进程间的通信。
消息队列MessageQueue:消息队列是由消息的链表,存放在内核中并由消息队列标识符标识。消息队列克服了信号传递信息少、管道只能承载无格式字节流以及缓冲区大小受限等缺点。
共享存储SharedMemory:共享内存就是映射一段能被其他进程所访问的内存,这段共享内存由一个进程创建,但多个进程都可以访问。共享内存是最快的 IPC 方式,它是针对其他进程间通信方式运行效率低而专门设计的。它往往与其他通信机制,如信号两,配合使用,来实现进程间的同步和通信。
信号量Semaphore:信号量是一个计数器,可以用来控制多个进程对共享资源的访问。它常作为一种锁机制,防止某进程正在访问共享资源时,其他进程也访问该资源。因此,主要作为进程间以及同一进程内不同线程之间的同步手段。
套接字Socket:套解口也是一种进程间通信机制,与其他通信机制不同的是,它可用于不同及其间的进程通信。
信号 ( sinal ) : 信号是一种比较复杂的通信方式,用于通知接收进程某个事件已经发生。

消息传递:
objc_msgSend

  • 通过对象的isa指针找到对象所属的class
  • 在class的方法缓存(objc_cache)中查找方法,如果没有继续往下
  • 在class的method_list中查找方法
  • 如果class中没有找到方法,则继续往它的super class中查找, 直到查找到根类
  • 一旦找到方法,就去执行对应的方法实现(IMP),并把方法添加到方法缓存中

在这个方法查找过程中,runtime引入了缓存机制,这是为了提高方法查找的效率,因为,如果调用的方法在根类中,那么每次方法调用都要沿着继承链去每个类的方法列表中查找一遍,效率无疑是低下的。这个方法缓存的实现,其实就是一个哈希表的存储,以selector name的哈希值为索引, 存储方法的实现(IMP),这样的查找效率较高,看到这里,可能有人会有疑问,既然每个class维护着一个方法缓存的哈希表,为什么还要维护一个方法列表method list呢?每次直接去哈希表里查找方法不是更快吗?
这其实是因为哈希表是一个无序表,而方法列表是一个有序列表,查找方法会顺着method list依次查找,这样就赋予了category一个特性:可以覆盖原本类的实现,而如果是使用哈希表,则无法保证顺序。

消息转发机制:
1 动态方法解析
+ (BOOL)resolveInstanceMethod:(SEL)sel
参数为那个未知的选择子,返回值表示这个类能否新增一个实例方法处理此选择子。假如尚未实现的方法不是实例方法而是类方法则运行期会调用另一个方法:+ (BOOL) resolveClassMethod:(SEL)selector。使用这种方法的前提是:相关方法的实现代码已经写好,只等着运行的时候动态插入到类里面就可以了。此方案常用来实现@dynamic属性
+ (BOOL)resolveClassMethod:(SEL)sel
2 备援接收者
-(id)forwardingTargetForSelector:(SEL)aSelector
当前接收者还有第二次机会能处理未知的选择子,这一步中,运行期会问它:能不能把这条消息转发给其他接收者来处理。与该步骤对应的处理方法
若当前接收者能找到备援对象,则将其返回,若找不到就返回nil。通过此方案我们可以用“组合”来模拟出“多重继承”的某些特性(因为OC属于单继承,一个字类只能继承一个基类)。在一个对象内部,可能还有一系列其他对象,该对象可能由此方法将能够处理某选择子的相关内部对象返回,这样的话,在外界看来,好像该对象亲自处理了这些消息。
3 完整的消息转发
如果转发算法已经到了这一步的话,那么唯一能做的就是启用完整的消息转发机制了。首先创建NSInvocation对象,把与尚未处理的那条消息有关的全部细节都封装与其中。此对象包含选择子、目标(target)、参数。在出发NSInvocation对象时“消息派发系统”将亲自出马,把消息指派给目标对象
//获取方法签名进入下一步,进行消息转发
- (NSMethodSignature )methodSignatureForSelector:(SEL)aSelector;
- (void)forwardInvocation:(NSInvocation
)anInvocation;
实现了此方法若发现某调用操作不应由本类处理,则需调用超类的同名方法。这样的话继承体系中的每个类都有机会处理此调用请求,直至NSObject。如果最后调用了NSObject类的方法,那么该方法还会继续调用“doesNotRecognizeSelector:”以抛出异常,此异常表明选择子最终未能得道处理

Object-C的类可以多重继承么?可以实现多个接口么?Category是什么?重写一个类的方式用继承好还是分类好?为什么?

  • Object-C的类不可以多重继承,可以实现多个接口,通过实现多个接口可以完成C++的多重继承
  • Category是类别,取决于业务场景,因为如果人家有原方法,就可能被其子类继承,分类重写,子类方法应该会覆盖掉父类方法
  • https://www.jianshu.com/p/87cfbdda0a68

分类特点:
1.分类是用于给原有类添加方法的,因为分类的结构体指针中,没有属性列表,只有方法列表。原则上讲它只能添加方法, 不能添加属性(成员变量),实际上可以通过其它方式添加属性 ;
2.分类中的可以写@property, 但不会生成setter/getter方法, 也不会生成实现以及私有的成员变量,会编译通过,但是引用变量会报错;
3.如果分类中有和原有类同名的方法, 会优先调用分类中的方法, 就是说会忽略原有类的方法,同名方法调用的优先级为 分类 > 本类 > 父类;
4.如果多个分类中都有和原有类中同名的方法, 那么调用该方法的时候执行谁由编译器决定;编译器会执行最后一个参与编译的分类中的方法
5.运行时决议
6.同名分类方法生效取决于编译顺序

#import 跟#include 又什么区别,@class呢, #import<> 跟 #import又什么区别?

  • import是Objective-C导入头文件的关键字

  • include是C/C++导入头文件的关键字,使用#import头文件会自动只导入一次,不会重复导入,相当于#include和#pragma once

  • @class告诉编译器某个类的声明,当执行时,才去查看类的实现文件,可以解决头文件的相互包含
  • import<>用来包含系统的头文件,#import“”用来包含用户头文件

属性readwrite,readonly,assign,retain,copy,nonatomic 各是什么作用,在那种情况下用?

  • readwrite 是可读可写特性;需要生成getter方法和setter方法时
  • readonly 是只读特性 只会生成getter方法 不会生成setter方法 ;不希望属性在类外改变
  • assign 是赋值特性,setter方法将传入参数赋值给实例变量;仅设置变量时;
  • retain 表示持有特性,setter方法将传入参数先保留,再赋值,传入参数的retaincount会+1;
  • copy 表示赋值特性,setter方法将传入对象复制一份;需要完全一份新的变量时
  • nonatomic 非原子操作,决定编译器生成的setter getter是否是原子操作,atomic表示多线程安全,一般使用nonatomic

写一个 setter 方法用于完成
@property (nonatomic, retain) NSString *name

  1. // retain
  2. - (void)setName:(NSString *)str {
  3. [str retain];
  4. [_name release];
  5. _name = str;
  6. }
  7. // copy
  8. - (void)setName:(NSString *)str {
  9. id t = [str copy];
  10. [_name release];
  11. _name = t;
  12. }

原子(atomic)跟非原子(non-atomic)属性有什么区别?

  • atomic提供多线程安全。是防止在写未完成的时候被另外一个线程读取,造成数据错误
  • non-atomic:在自己管理内存的环境中,解析的访问器保留并自动释放返回的值,如果指定了 nonatomic ,那么访问器只是简单地返回这个值

内存管理的几条原则时什么?按照默认法则.那些关键字生成的对象需要手动释放?在和property结合的时候怎样有效的避免内存泄露?

  • 遵循Cocoa Touch的使用原则;
  • 内存管理主要要避免“过早释放”和“内存泄漏”,对于“过早释放”需要注意@property设置特性时,一定要用对特性关键字,对于“内存泄漏”,一定要申请了要负责释放,要细心。
  • 关键字alloc 或new 生成的对象需要手动释放;
  • 设置正确的property属性,对于retain需要在合适的地方释放
  • 谁申请的谁释放

类别和类扩展的区别?

  • category和extensions的不同在于: 后者可以添加属性,另外后者添加的方法是必须要实现的
  • extensions可以认为是一个私有的Category

Objective-C堆和栈的区别?

  • 对于栈来讲,是由编译器自动管理,无需我们手工控制;对于堆来说,释放工作由程序员控制,容易产生memory leak
  • 栈:在Windows下,栈是向低地址扩展的数据结构,是一块连续的内存的区域。这句话的意思是栈顶的地址和栈的最大容量是系统预先规定好的,在 WINDOWS下,栈的大小是2M(也有的说是1M,总之是一个编译时就确定的常数),如果申请的空间超过栈的剩余空间时,将提示overflow。因此,能从栈获得的空间较小
  • 堆:堆是向高地址扩展的数据结构,是不连续的内存区域。这是由于系统是用链表来存储的空闲内存地址的,自然是不连续的,而链表的遍历方向是由低地址向高地址。堆的大小受限于计算机系统中有效的虚拟内存。由此可见,堆获得的空间比较灵活,也比较大
  • 碎片问题:对于堆来讲,频繁的new/delete势必会造成内存空间的不连续,从而造成大量的碎片,使程序效率降低。对于栈来讲,则不会存在这个问题,因为栈是先进后出的队列,他们是如此的一一对应,以至于永远都不可能有一个内存块从栈中间弹出
  • 分配方式:堆都是动态分配的,没有静态分配的堆。栈有2种分配方式:静态分配和动态分配。静态分配是编译器完成的,比如局部变量的分配。动态分配由alloca函数进行分配,但是栈的动态分配和堆是不同的,他的动态分配是由编译器进行释放,无需我们手工实现
  • 分配效率:栈是机器系统提供的数据结构,计算机会在底层对栈提供支持:分配专门的寄存器存放栈的地址,压栈出栈都有专门的指令执行,这就决定了栈的效率比较高。堆则是C/C++函数库提供的,它的机制是很复杂的

简述iOS的系统架构

  • 核心操作系统层 theCore OS layer
  • 核心服务层 theCore Services layer
  • 媒体层 heMedia layer
  • Cocoa 界面服务层 the Cocoa Touch layer

控制器的生命周期

  • initWithNibName
  • awakeFromNib
  • loadView
  • viewDidLoad
  • viewWillAppear
  • viewWillLayoutSubviews
  • viewDidLayoutSubviews
  • viewDidAppear
  • viewWillDisappear
  • viewDidDisappear
  • didReceiveMemoryWarning
  • dealloc

一个UIView从被声明到渲染在屏幕上经历了什么?

  • 首先一个视图由CPU进行Frame布局,准备视图和图层的层级关系,查询是否有重写drawRect:或drawLayer:inContext:方法,注意:如果有重写的话,这里的渲染是会占用CPU进行处理的
  • CPU会将处理视图和图层的层级关系打包,通过IPC(内部处理通信)通道提交给渲染服务,渲染服务由OpenGL ES和GPU组成
  • 渲染服务首先将图层数据交给OpenGL ES进行纹理生成和着色。生成前后帧缓存,再根据显示硬件的刷新频率,一般以设备的VSync信号和CADisplayLink为标准,进行前后帧缓存的切换
  • 最后,将最终要显示在画面上的后帧缓存交给GPU,进行采集图片和形状,运行变换,应用纹理和混合。最终显示在屏幕上

掉帧是怎么产生的?

  • 在 VSync 信号到来后,系统图形服务会通过 CADisplayLink 等机制通知 App,App 主线程开始在 CPU 中计算显示内容,比如视图的创建、布局计算、图片解码、文本绘制等
  • 随后 CPU 会将计算好的内容提交到 GPU 去,由 GPU 进行变换、合成、渲染
  • 随后 GPU 会把渲染结果提交到帧缓冲区去,等待下一次 VSync 信号到来时显示到屏幕上。由于垂直同步的机制,如果在一个 VSync 时间内,CPU 或者 GPU 没有完成内容提交,则那一帧就会被丢弃,等待下一次机会再显示,而这时显示屏会保留之前的内容不变。这就是界面卡顿的原因
  • CPU 和 GPU 不论哪个阻碍了显示流程,都会造成掉帧现象。所以开发时,也需要分别对 CPU 和 GPU 压力进行评估和优化

解决方案

  1. 主线程卡顿监控:通过子线程监测主线程的 runLoop,判断两个状态区域之间的耗时是否达到一定阈值
  2. FPS监控:要保持流畅的UI交互,App 刷新率应该当努力保持在 60fps。监控实现原理比较简单,通过记录两次刷新时间间隔,就可以计算出当前的 FPS

离屏渲染
如果要在显示屏上显示内容,我们至少需要一块与屏幕像素数据量一样大的frame buffer,作为像素数据存储区域,而这也是GPU存储渲染结果的地方。如果有时因为面临一些限制,无法把渲染结果直接写入frame buffer,而是先暂存在另外的内存区域,之后再写入frame buffer,那么这个过程被称之为离屏渲染
对于类似这种“新开一块CGContext来画图“的操作,有很多文章和视频也称之为“离屏渲染”(因为像素数据是暂时存入了CGContext,而不是直接到了frame buffer)。进一步来说,其实所有CPU进行的光栅化操作(如文字渲染、图片解码),都无法直接绘制到由GPU掌管的frame buffer,只能暂时先放在另一块内存之中,说起来都属于“离屏渲染”
自然我们会认为,因为CPU不擅长做这件事,所以我们需要尽量避免它,就误以为这就是需要避免离屏渲染的原因。但是根据苹果工程师的说法,CPU渲染并非真正意义上的离屏渲染。另一个证据是,如果你的view实现了drawRect,此时打开Xcode调试的“Color offscreen rendered yellow”开关,你会发现这片区域不会被标记为黄色,说明Xcode并不认为这属于离屏渲染。
其实通过CPU渲染就是俗称的“软件渲染”,而真正的离屏渲染发生在GPU
对于每一层layer,要么能找到一种通过单次遍历就能完成渲染的算法,要么就不得不另开一块内存,借助这个临时中转区域来完成一些更复杂的、多次的修改/剪裁操作
渲染性能的调优,其实始终是在做一件事:平衡CPU和GPU的负载,让他们尽量做各自最擅长的工作

KVC底层实现机制
- (void)setValue:(nullable id)value forKey:(NSString *)key;

  1. 代码通过-set方法赋值,如果没有找到,还会去查找 -_set
  2. 否则,检查+(BOOL)accessInstanceVariablesDirectly方法,如果你重写了该方法并使其返回NO,则KVC下一步会执行setValue:forUndefinedKey:,默认抛出异常
  3. 最后,KVC会依次搜索该类中名为的成员变量
  4. 如果都没有,则执行setValue:forUndefinedKey:方法,默认抛出异常

- (nullable id)valueForKey:(NSString *)key;

  1. 首次依次查找-get,-,-is代码通过getter方法获取值
  2. 否则,查找-countOf,-objectInAtIndex:和-AtIndexes:方法,如果count方法和另外两个中的一个被找到,返回一个能响应所有NSArray方法的代理集合,简单来说就是可以当NSArray用
  3. 否则,查找-countOf,-enumeratorOf和-memberOf:方法,如果三个都能找到,返回一个能响应所有NSSet方法的代理集合,简单来说就是可以当NSSet使用
  4. 否则,依次搜索该类中名为的成员变量,返回该成员变量的值
  5. 如果都没有,则执行valueForUndefinedKey:方法,默认抛出异常

如何避免KVC修改readonly属性?
在没有setter方法时,会检查+(BOOL)accessInstanceVariablesDirectly来决定是否搜索相似成员变量,因此只需要重写该方法并返回NO即可
如何校验KVC的正确性?
通过- (BOOL)validateValue:(inout id nullable * nonnull)ioValue forKey:(NSString )inKey error:(out NSError )outError;这个方法的默认实现是去探索类里面是否有一个这样的方法-(BOOL)validate:error:,如果有这个方法,就调用这个方法来返回,没有的话就直接返回YES
注意:在KVC设值的时候,并不会主动调用该方法去校验,需要开发者手动调用校验,意味着即使实现此方法,也可以赋值成功
常见应用场景*

  1. 可以灵活的使用字符串动态取值和设值,但通过KVC操作对象的性能比getter和setter更差
  2. 访问和修改私有属性,最常见的就是修改UITextField中的placeHolderText
  3. 通过- (void)setValuesForKeysWithDictionary:字典转model,如股票字段
  4. 当对容器类使用KVC时,valueForKey:将会被传递给容器中的每一个对象,而不是容器本身进行操作,由此我们可以有效的提取容器中每个对象的指定属性值集合
  5. 使用函数操作容器中的对象,快速对各对象中的基础类型属性做运算,如@avg,@count ,@max ,@min @sum

KVO底层实现机制
当观察者对对象A注册一个监听时,系统此时会动态创建一个名为NSKVONotifying_A的新类,该类继承自对象A原本的类,并重写被观察者的setter方法,在原setter方法的调用前后通知观察者值的改变
然后将对象A的isa指针(isa指针告诉Runtime这个对象的类是什么)指向NSKVONotifying_A这个新类,那么对象A就变成了新创建新类的实例。 不仅如此,Apple还重写了-class方法来隐藏该新类,让人们以为注册前后对象A的类并没有改变,但实际上如果手动新建一个NSKVONotifying_A类,在观察者运行到注册时,便会引起重复类崩溃
从上述实现原理来看,很明显可以知道,如果没有通过setter赋值,直接赋值该成员变量是不会触发KVO机制的,但是KVC除外,这也侧面证明了KVC与KVO是有内在联系的

alloc/init与new
new只能使用默认的init初始化,而alloc可以使用其他初始化方法,因为显示调用总比隐式调用好,所以往往使用alloc/init来初始化

@”hello”和[NSString stringWithFormat:@”hello”]有何区别?

  1. NSString *A = @"hello";
  2. NSString *B = @"hello";
  3. NSString *C = [NSString stringWithFormat:@"hello"];
  4. NSString *D = [NSString stringWithFormat:@"hello"];
  5. NSString *E = [[NSString alloc] initWithFormat:@"hello"];
  6. NSString *F = [[NSString alloc] initWithFormat:@"hello"];
  7. NSLog(@"A=%p\n B=%p\n C=%p\n D=%p\n E=%p\n F=%p\n", A, B, C, D, E, F);
  8. // 结果
  9. A=0x104ba0070
  10. B=0x104ba0070
  11. C=0xdbd16c40a2e07e99
  12. D=0xdbd16c40a2e07e99
  13. E=0xdbd16c40a2e07e99
  14. F=0xdbd16c40a2e07e99

@”hello”位于常量池中,可重复使用,其中A和B指向的都是同一份内存地址。 而stringWithFormat或initWithFormat是在运行时创建出来的,保存在运行时内存(即堆内存),它们在堆里面请求对应的值,如果存在,系统便不再分配地址

Propoty修饰符
具体可分为四类:线程安全、读写权限、内存管理和指定读写方法

线程安全(atomic,nonatomic)
如果不写该类修饰符,默认就是atomic。两者最大的区别就是决定编译器生成的getter/setter方法是否属于原子操作,如果自己写了getter/setter方法,此时用什么都一样。 对于atomic来说,getter/setter方法增加了锁来确保操作的完整性,不受其他线程影响。例如线程A的getter方法运行到一半,线程B调用setter方法,那么线程A还是能得到一个完整的Value。 而对于nonatomic来说,多个线程能同时访问操作,就无法保证是否是完整的Value,还会引发脏数据。但是nonatomic更快,开发中往往在可控情况下安全换效率

注意:atomic并不能完全保证线程安全,只能保证数据操作的线程安全,例如线程A使用getter方法,同时线程B、C使用setter方法,那最后线程A获取到的值有三种可能:原始值、B set的值或者C set的值;又例如线程A使用getter方法,线程B同时调用release方法,由于release方法并没有加锁,所以有可能会导致cash。

读写权限(readonly,readwrite)
readonly只读属性,只会生成getter方法,不会生成setter方法。 readwrite读写属性,会生成getter/setter方法,默认是该修饰符

内存管理(strong,weak,assign,copy)
strong强引用,适用于对象,引用计数+1,对象默认是该修饰符。 weak弱引用,为这种属性设置新值时,设置方法既不释放旧值,也不保留新值,不会使引用计数加1。当所指对象被销毁时,指针会自动被置为nil,防止野指针
assgin适用于基础数据类型,如NSIntger,CGFloat,int等,只进行简单赋值,基础数据类型默认是该修饰符。如果用此修饰符修饰对象,对象被销毁时,并不会置空,会造成野指针。 copy是为了解决上下文的异常依赖,实际赋值类型不可变对象时,浅拷贝;可变对象时,深拷贝

指定读写方法(setter=,getter=)
给getter/setter方法起别名,可以不一致,并且可以与其他属性的getter/setter重名,例如Person类中定义如下

  1. @property (nonatomic, copy, setter=setNewName:, getter=oldName) NSString *name; @property (nonatomic, copy) NSString *oldName;

那么此时p1.oldName始终是_name的值,而如果声明的顺序交换,此时p1.oldName就是_oldName的值了,如果想得到_name的值,使用p1.name即可,但是此时不能使用-setName:。所以别名都是有意义且不重复的,避免一些想不到的问题

  • strong和copy的区别

strong是浅拷贝,仅拷贝指针并增加引用计数;而copy在对于实际赋值对象是可变对象时,是深拷贝。不可变对象使用copy修饰,如NSString、NSArray、NSSet等;可变对象使用strong修饰,如NSMutableString、NSMutableArray、NSMutableSet等,这是为什么呢? 由于父类属性可以指向子类对象,试想这样一个例子:

  1. @interface Person : NSObject
  2. @property (nonatomic, strong) NSString *name;
  3. @end
  4. NSMutableString *mutableName = [NSMutableString stringWithFormat:@"hello"];
  5. p.name = mutableName;
  6. [mutableName appendString:@" world"];

由于Person.name使用的strong修饰,它对于赋值对象进行的浅拷贝,那么Person.name此时实际指向与mutableName指向的同一块的内存区,如果将mutableName的内容修改,此时Person.name也会修改,这并不是我们想要的,所以我们使用copy来修饰,这样即使赋值对象是一个可变对象,也会在setter方法中copy一份不可变对象再赋值。 而对于可变对象的属性来说,如果使用copy修饰,从上面可知会得到一个不可变对象再赋值,那么如果你想要修改对象内容的时候,就会抛出异常,所以我们用strong。

  • assgin和weak的区别

assgin用于基础类型,可以修饰对象,但是这个对象在销毁后,这个指针并不会置空,会造成野指针错误。 weak用于对象,无法修饰基础类型,并且在对象销毁后,指针会自动置为nil,不会引起野指针崩溃。

  • var、getter、setter 是如何生成并添加到这个类中的?

完成属性定义后,编译器会自动编写访问这些属性所需的方法,此过程叫做“自动合成”(autosynthesis)。需要强调的是,这个过程由编译器在编译期执行,所以编辑器里看不到这些“合成方法”(synthesized method)的源代码。除了生成方法代码 getter、setter 之外,编译器还要自动向类中添加适当类型的实例变量,并且在属性名前面加下划线,以此作为实例变量的名字。也可以在类的实现代码里通过 @synthesize 语法来指定实例变量的名字。

  • @protocol 和 category 中如何使用 @property

在 protocol 中使用 property 只会生成 setter 和 getter 方法声明,我们使用属性的目的,是希望遵守我协议的对象能实现该属性。 category 使用 @property 也是只会生成 setter 和 getter 方法的声明,如果我们真的需要给 category 增加属性的实现,需要借助于Runtime的关联对象

Weak的实现原理
在Runtime中,为了管理所有对象的引用计数和weak指针,创建了一个全局的SideTables,实际是一个hash表,里面都是SideTable的结构体,并且以对象的内存地址作为key,SideTable部分定义

  1. struct SideTable {
  2. //保证原子操作的自旋锁(后面自旋锁因为不安全改为了互斥锁)
  3. spinlock_t slock;
  4. //保存引用计数的hash表
  5. RefcountMap refcnts;
  6. //用于维护weak指针的结构体
  7. weak_table_t weak_table;
  8. ....
  9. };

其中用来维护weak指针的结构体weak_table_t是一个全局表,其定义如下

  1. struct weak_table_t {
  2. //保存所有弱引用表的入口,包含所有对象的弱引用表
  3. weak_entry_t *weak_entries;
  4. //存储空间
  5. size_t num_entries;
  6. //参与判断引用计数辅助量
  7. uintptr_t mask;
  8. //hash key 最大偏移值
  9. uintptr_t max_hash_displacement;
  10. };

其中所有的weak指针正是存在weak_entry_t中,其部分定义如下

  1. struct weak_entry_t {
  2. //被指对象的地址。前面循环遍历查找的时候就是判断目标地址是否和他相等。
  3. DisguisedPtr<objc_object> referent;
  4. union {
  5. struct {
  6. //可变数组,里面保存着所有指向这个对象的弱引用的地址。当这个对象被释放的时候,referrers里的所有指针都会被设置成nil。
  7. weak_referrer_t *referrers;
  8. uintptr_t out_of_line_ness : 2;
  9. uintptr_t num_refs : PTR_MINUS_2;
  10. uintptr_t mask;
  11. uintptr_t max_hash_displacement;
  12. };
  13. struct {
  14. // out_of_line_ness field is low bits of inline_referrers[1]
  15. weak_referrer_t inline_referrers[WEAK_INLINE_COUNT];
  16. };
  17. };
  18. ...
  19. };

weak由于不增加引用计数,所以不能在SideTable中与引用计数表放在一起,Runtime单独使用了一个全局hash表weak_table_t来管理weak,其中底层结构体weak_entry_t以weak指向的对象内存地址为key,value是一个存储该对象所有weak指针的数组。当这个对象dealloc时,假设该对象的内存地址为a,查出对应的SideTable,搜索key为a对应的指针数组,并且遍历数组将所有weak对象置为nil,并清除记录

  1. //创建weak对象
  2. id __weak obj1 = obj;
  3. //Runtime会调用如下方法初始化
  4. id objc_initWeak(id *location, id newObj)
  5. {
  6. //如果对象实例为nil,当前weak对象直接置空
  7. if (!newObj) {
  8. *location = nil;
  9. return nil;
  10. }
  11. return storeWeak<DontHaveOld, DoHaveNew, DoCrashIfDeallocating>
  12. (location, (objc_object*)newObj);
  13. }
  14. //更新指针指向
  15. static id storeWeak(id *location, objc_object *newObj)
  16. {
  17. assert(haveOld || haveNew);
  18. if (!haveNew) assert(newObj == nil);
  19. Class previouslyInitializedClass = nil;
  20. id oldObj;
  21. SideTable *oldTable;
  22. SideTable *newTable;
  23. //查询当前weak指针原指向的oldSideTable与当前newObj的newSideTable
  24. retry:
  25. if (haveOld) {
  26. oldObj = *location;
  27. oldTable = &SideTables()[oldObj];
  28. } else {
  29. oldTable = nil;
  30. }
  31. if (haveNew) {
  32. newTable = &SideTables()[newObj];
  33. } else {
  34. newTable = nil;
  35. }
  36. .....
  37. //解除weak指针在旧对象中注册
  38. if (haveOld) {
  39. weak_unregister_no_lock(&oldTable->weak_table, oldObj, location);
  40. }
  41. //添加weak到新对象的注册
  42. if (haveNew) {
  43. newObj = (objc_object *)
  44. //这个地方仍然需要newObj来核对内存地址来找到weak_entry_t,从而删除
  45. weak_register_no_lock(&newTable->weak_table, (id)newObj, location,
  46. crashIfDeallocating);
  47. if (newObj && !newObj->isTaggedPointer()) {
  48. newObj->setWeaklyReferenced_nolock();
  49. }
  50. *location = (id)newObj;
  51. } else {}
  52. SideTable::unlockTwo<haveOld, haveNew>(oldTable, newTable);
  53. return (id)newObj;
  54. }

loadinitialize方法的区别是什么?
调用方式
1、load是根据函数地址直接调用
2、initialize是通过objc_msgSend调用
调用时刻
1、load是runtime加载类、分类的时候调用(只会调用一次)
2、initialize是类第一次接收到消息的时候调用, 每一个类只会initialize一次(如果子类没有实现initialize方法, 会调用父类的initialize方法, 所以父类的initialize方法可能会调用多次)
load和initializee的调用顺序
1、load:
先调用类的load, 在调用分类的load
先编译的类, 优先调用load, 调用子类的load之前, 会先调用父类的load
先编译的分类, 优先调用load
2、initialize
先初始化分类, 后初始化子类
通过消息机制调用, 当子类没有initialize方法时, 会调用父类的initialize方法, 所以父类的initialize方法会调用多次

进程分配的资源有哪些
一般情况下,包括文本区域(text region)、数据区域(data region)和堆栈(stack region)
文本区域存储处理器执行的代码;数据区域存储变量和进程执行期间使用的动态分配的内存;堆栈区域存储着活动过程调用的指令和本地变量。

进程是怎么进行切换的,需要保存哪些东西
在切换时,一个进程存储在处理器各寄存器中的中间数据叫做进程的上下文,所以进程的 切换实质上就是被中止运行进程与待运行进程上下文的切换。在进程未占用处理器时,进程 的上下文是存储在进程的私有堆栈中的。
进程上下文切换由以下4个步骤组成:
1)决定是否作上下文切换以及是否允许作上下文切换。包括对进程调度原因的检查分析,以及当前执行进程的资格和CPU执行方式的检查等。在操作系统中,上下文切换程序并不是每时每刻都在检查和分析是否可作上下文切换,它们设置有适当的时机
(2)保存当前执行进程的上下文。这里所说的当前执行进程,实际上是指调用上下文切换程序之前的执行进程。如果上下文切换不是被那个当前执行进程所调用,且不属于该进程,则所保存的上下文应是先前执行进程的上下文,或称为“老”进程上下文。显然,上下文切换程序不能破坏“老”进程的上下文结构
(3)使用进程调度算法,选择一处于就绪状态的进程
(4)恢复或装配所选进程的上下文,将CPU控制权交到所选进程手中
进程间通信的方式——信号、管道、消息队列、共享内存

HashMap
HashMap中除了哈希算法之外,有两个参数影响了性能:初始容量和加载因子。初始容量是哈希表在创建时的容量,加载因子是哈希表在其容量自动扩容之前可以达到多满的一种度量。
在维基百科来描述加载因子:
对于开放定址法,加载因子是特别重要因素,应严格限制在0.7-0.8以下。超过0.8,查表时的CPU缓存不命中(cache missing)按照指数曲线上升。因此,一些采用开放定址法的hash库,如Java的系统库限制了加载因子为0.75,超过此值将resize散列表。
在设置初始容量时应该考虑到映射中所需的条目数及其加载因子,以便最大限度地减少扩容rehash操作次数,所以,一般在使用HashMap时建议根据预估值设置初始容量,以便减少扩容操作。
选择0.75作为默认的加载因子,完全是时间和空间成本上寻求的一种折衷选择。

UIView和 CALayer是什么关系?
UIView是CALayer的delegate
UIView: 继承与UIResponder,能响应点击事件
CALayer:不能响应点击事件,主要负责绘制和显示,并且UIView的尺寸样式由内部的Layer提供

死锁的四个条件:

  • 互斥条件:资源是独占的且排他使用,进程互斥使用资源,即任意时刻一个资源只能给一个进程使用,其他进程若申请一个资源,而该资源被另一进程占有时,则申请者等待直到资源被占有者释放。
  • 不可剥夺条件:进程所获得的资源在未使用完毕之前,不被其他进程强行剥夺,而只能由获得该资源的进程资源释放。
  • 请求和保持条件:进程每次申请它所需要的一部分资源,在申请新的资源的同时,继续占用已分配到的资源。
  • 循环等待条件:在发生死锁时必然存在一个进程等待队列{P1,P2,…,Pn},其中P1等待P2占有的资源,P2等待P3占有的资源,…,Pn等待P1占有的资源,形成一个进程等待环路,环路中每一个进程所占有的资源同时被另一个申请,也就是前一个进程占有后一个进程所深情地资源。

扩展

  1. 关于iOS11种锁: https://www.jianshu.com/p/b1edc6b0937a
  2. 不再安全的自旋锁: https://blog.ibireme.com/2016/01/16/spinlock_is_unsafe_in_ios/

平衡二叉树
它是一棵空树或它的左右两个子树的高度差的绝对值不超过1,并且左右两个子树都是一棵平衡二叉树。这个方案很好的解决了二叉查找树退化成链表的问题,把插入,查找,删除的时间复杂度最好情况和最坏情况都维持在O(logN)。但是频繁旋转会使插入和删除牺牲掉O(logN)左右的时间,不过相对二叉查找树来说,时间上稳定了很多。

动态库与静态库
库:实现了某一类功能的若干函数和二进制代码的集合。库的后缀名在不同平台上表现不同:
Windows:静态库:xxx.lib && 动态库:xxx.dll
Linux:静态库:xxx.a && 动态库:xxx.so
在xcode种 .framework可能是动态也可能为静态
库分为静态库和动态库,与之对应的操作是静态链接和动态链接,静态库不能采用动态链接,同理,动态库也不能采用静态链接。

静态库:在链接的时候,函数库被完整的拷贝到可执行文件中,对应的链接方式成为静态链接,采用gcc -static指令。这里的可执行文件可以使单纯的一个a.out,也可以是一整个app或者程序。
动态库:相对于静态库,动态库并没有将库中所有的数据复制到可执行文件中,在程序运行的时候才会被动态载入。
备注
动态链接可以理解成只是将声明文件复制到了程序中。在运行时根据预先设置的动态库的位置和这部分声明来调用对应的库。所以说动态库也叫共享库,共享在整个系统中。对于iOS系统而言,所有系统提供的.framework都是动态库且在各个app之间共享。但是iOS基于沙盒模式,其不允许用户自己创建动态的.framework。即使在iOS8之后可以创建动态framework,但其本质还是将动态库放入了app的沙盒中。iOS不允许包含自定义动态库的app上架到app store,所以iOS中的动态库只能应用在不上架app store的企业应用中。

静态库和动态库的区别和特点
静态库:
1、一旦链接完成,执行程序就和函数库没有任何关联
2、占用空间和资源,拷贝多次就会占用多份资源
3、会导致升级不便,一个地方修改就需要全量更新
动态库:
1、可以实现进程之间的资源共享
2、动态库把对一些库函数的链接载入推迟到程序运行的时期
3、将一些程序升级变得简单
4、可以通过显示调用做到链接载入完全由程序员在程序代码中控制
5、动态库的创建直接使用编译器即可创建动态库,不需要打包工具(ar、lib.exe

不手动指定autoreleasepool的前提下,一个autorealese对象在什么时刻释放?(比如在一个vc的viewDidLoad中创建)
分两种情况:手动干预释放时机、系统自动去释放
1.手动干预释放时机—指定autoreleasepool 就是所谓的:当前作用域大括号结束时释放
2.系统自动去释放—不手动指定autoreleasepool
Autorelease对象会在当前的 runloop 迭代结束时释放
如果在一个vc的viewDidLoad中创建一个 Autorelease对象,那么该对象会在 viewDidAppear 方法执行前就被销毁了

苹果是如何实现autoreleasepool的?
autoreleasepool以一个队列数组的形式实现,主要通过下列三个函数完成
objc_autoreleasepoolPush
objc_autoreleasepoolPop
objc_aurorelease
看函数名就可以知道,对autorelease分别执行push,和pop操作。销毁对象时执行release操作

苹果是用什么方式实现对一个对象的KVO?
当你观察一个对象时,一个新的类会被动态创建。这个类继承自该对象的原本的类,并重写了被观察属性的 setter 方法。重写的 setter 方法会负责在调用原 setter 方法之前和之后,通知所有观察对象:值的更改。最后通过 isa 混写(isa-swizzling) 把这个对象的 isa 指针 ( isa 指针告诉 Runtime 系统这个对象的类是什么 ) 指向这个新创建的子类,对象就神奇的变成了新创建的子类的实例。
键值观察通知依赖于 NSObject 的两个方法: willChangeValueForKey: 和 didChangevlueForKey: 。在一个被观察属性发生改变之前, willChangeValueForKey: 一定会被调用,这就 会记录旧的值。而当改变发生后, didChangeValueForKey: 会被调用,继而 observeValueForKey:ofObject:change:context: 也会被调用。可以手动实现这些调用,但很少有人这么做。一般我们只在希望能控制回调的调用时机时才会这么做。大部分情况下,改变通知会自动调用。
KVO 在实现中通过 isa 混写(isa-swizzling) 把这个对象的 isa 指针 ( isa 指针告诉 Runtime 系统这个对象的类是什么 ) 指向这个新创建的子类,对象就神奇的变成了新创建的子类的实例。

Objective-C 的对象模型(深入源码)

  1. struct objc_class {
  2. struct objc_class *isa;
  3. struct objc_class *super_class;
  4. const char *name;
  5. long version;
  6. long info;
  7. long instance_size;
  8. struct objc_ivar_list *ivars;
  9. #if defined(Release3CompatibilityBuild)
  10. struct objc_method_list *methods;#else
  11. struct objc_method_list **methodLists;
  12. #endif
  13. struct objc_cache *cache;
  14. struct objc_protocol_list *protocols;
  15. };

可以看到obj_class是一个结构体,它包含了所有运行时需要的有关类的信息,包括这个类的父类是什么类,实例变量,方法,协议等。有趣的是,obj_class中也有一个isa属性,那么它又指向哪里呢?它指向的是一个叫做metaclass的对象,并且类型也是obj_class。所以实例化一个类会有两个对象:本身和metaclass对象。这样做的目的是把实例方法的信息保存到自己本身的类中,而把类方法保存到metaclass类里。那么metaclass中的isa指向哪里呢?因为metaclass类是没有metaclass方法的,所有就不需要再多一个类来保存metaclass类的方法信息,因此,metaclass对象的isa指向自己,形成一个闭环结构。

iOS字典大致实现原理
NSDictionary 是使用hash表来实现key和value之间的映射和存储的,其底层其实是一个hash表,绝大多数语言里的字典都是用hash表实现。
1.根据key值计算出它的hash值
2.假设箱子个数为n,那这个键值对应该放在第(h%n)个箱子中
3.如果该箱子已经有了键值对,就使用开放寻址法或者拉链法解决冲突
在使用拉链法解决哈希冲突的时候,每一个箱子都是一个链表,属于同一个箱子的所有键值对都会排列在链表里

扩展:哈希表中有一个重要属性:哈希因子
它是用来衡量哈希表的空满程度,一定程度上可以提现查询的效率:负载因子=总键值对/箱子个数
哈希因子越大,意味着哈希表越满,越容易导致冲突,性能越低。因此,一般来说,当负载因子大于某个常数(1或者0.75),哈希表会自动扩容。

重哈希概念: 哈希表自动扩容的时候,一般会创建两倍于原来的箱子个数,因此,即使key的哈希值不变,对箱子的个数取余的结果也会发生改变,键值对存放的位置也会发生改变,这个过程也被成为重哈希。哈希表的扩容并不总是能有效解决负载因子过大的问题。假设,所有key的哈希值一样,那么即使扩容以后他们的位置不会变化,虽然负载因子会降低,但是实际存储每个箱子的链表长度不会改变,也不会提升性能。

总结:
1.如果哈希表本来箱子就比较多,扩容时需要重哈希并移动数据,性能影响大。
2.如果哈希设计不合理,哈希表在极端情况下回变成线性表,性能极低。