-
1. 常见的属性修饰符有哪些,使用copy应该注意些什么
-
MRC下:
assign、retain、copy、readwrite、readonly、nonatomic、atomic等。 -
ARC下:
assign、strong、weak、copy、readwrite、readonly、nonatomic、atomic、nonnull、nullable、null_resettable、_Null_unspecified等。
下面分别解释
-
assign:用于基本数据类型,不更改引用计数。如果修饰对象(对象在堆需手动释放内存,基本数据类型在栈系统自动释放内存),会导致对象释放后指针不置为nil 出现野指针。 -
retain:和strong一样,释放旧对象,传入的新对象引用计数+1;在MRC中和release成对出现。 -
strong:在ARC中使用,告诉系统把这个对象保留在堆上,直到没有指针指向,并且ARC下不需要担心引用计数问题,系统会自动释放。 -
weak:在被强引用之前,尽可能的保留,不改变引用计数;weak引用是弱引用,你并没有持有它;它本质上是分配一个不被持有的属性,当引用者被销毁(dealloc)时,weak引用的指针会自动被置为nil。可以避免循环引用。 -
copy:一般用来修饰不可变类型属性字段,如:NSString、NSArray、NSDictionary等。用copy修饰可以防止本对象属性受外界影响,在NSMutableString赋值给NSString时,修改前者 会导致 后者的值跟着变化。还有block也经常使用 copy 修饰符,但是其实在ARC中编译器会自动对block进行copy操作,和strong的效果是一样的。但是在MRC中方法内部的block是在栈区,使用copy可以把它放到堆区。 -
readwrite:可以读、写;编译器会自动生成setter/getter方法。 -
readonly:只读;会告诉编译器不用自动生成setter方法。属性不能被赋值。 -
nonatomic:非原子性访问。用nonatomic意味着可以多线程访问变量,会导致读写线程不安全。但是会提高执行性能。 -
atomic:原子性访问。编译器会自动生成互斥锁,对 setter 和 getter 方法进行加锁来保证属性的 赋值和取值 原子性操作是线程安全的,但不包括可变属性的操作和访问。比如我们对数组进行操作,给数组添加对象或者移除对象,是不在atomic的负责范围之内的,所以给被atomic修饰的数组添加对象或者移除对象是没办法保证线程安全的。原子性访问的缺点是会消耗性能导致执行效率慢。 -
nonnull:设置属性或方法参数不能为空,专门用来修饰指针的,不能用于基本数据类型。 -
nullable:设置属性或方法参数可以为空。 -
null_resettable:设置属性,get方法不能返回为空,set方法可以赋值为空。 -
_Null_unspecified:设置属性或方法参数不确定是否为空。
后四个属性应该主要就是为了提高开发规范,提示使用的人应该传什么样的值,如果违反了对规范值的要求,就会有警告。
- 2. 深拷贝和浅拷贝区别
深拷贝拷贝的是内容,浅拷贝拷贝的是指针。
简单来说,如果A拷贝了B,此时修改B,如果A也跟着改变了就是浅拷贝,如果A没变就是深拷贝。
如果自己去实现深拷贝就需要一层的遍历去拷贝,深拷贝的意义就在于,有可能被多人操作的数据,如果一处修改会影响到其他地方,使用深拷贝,把数据作为自己的,怎么修改都是自己的事情,不会影响到其他人。
- 对于不可变对象(不可变对象和不可变集合对象),进行copy都是指针拷贝,进行mutablecopy都是内容拷贝。
- 对于可变对象(可变对象和可变集合对象),进行copy和mutablecopy都是内容copy。
- 不管是可变对象还是不可变对象进行copy操作,产生的都是不可变的对象,进行mutablecopy操作产生的都是可变的对象。
- 对任何一个对象进行深拷贝,都是单层深拷贝。例如:对NSArray进行深拷贝,但是数组里面的元素还是指针拷贝。就是两个数组的地址不一样,但是里面的元素地址是一样的。所以要想让数组中的元素也是不一样的地址,那就对每个元素进行深拷贝
- 3. atomic 真的安全么,加的锁是哪种锁
不安全,它只是保证setter和getter方法内是安全的,一旦出了方法就需要外部控制了。
加的锁是spinlock_t。关于spinlock_t的实现在上面链接的文章中也有提到。
using spinlock_t = mutex_tt<LOCKDEBUG>;
class mutex_tt : nocopy_t {
os_unfair_lock mLock;
// 此处省略80多行代码...
}
Replacement for the deprecated OSSpinLock. Does not spin on contention but waits in the kernel to be woken up by an unlock.
译:os_unfair_lock是用来替代OSSpinLock这个自旋锁的互斥锁,不会自旋,在内核中等待被唤醒。所以说spinlock_t并不是如它的名字一般,而是个互斥锁。
- 4. iOS中内存管理是怎么样的
- 5. 自动释放池原理,本质
常见的内存泄漏有哪些
-
Retain Cycle,Block(闭包)强引用
- block可以使用
__weak和__Strong - 或者不直接使用
self - 将
self作为参数等方式去解决。 - 详情参考我的其他文章iOS Objective-C Block简介
- 闭包可以使用
weak和unowned等解决循环引用。
- block可以使用
-
NSTimer释放不当
NSTimer的循环因为是因为对target的强引用导致的,通常我们传self,所以解决这个循环引用主要有如下几种方式:- NSTimer 循环引用的原因和解决方案
- 不使用带target的timer,比如使用block
- 提前销毁timer,比如didMoveToParentViewController
- 使用中介者模式,将target换成其他对象,其他对象响应timer的调用事件
- 自定义封装timer,也是基于4
- 使用虚基类NSProxy
-
第三方提供方法造成的内存泄漏
使用时多注意,或者下载他的源码帮他改改(哈哈哈) -
CoreFoundation方式申请的内存,忘记释放
这个释放就好了
block 出现循环引用的原因
主要原因就是block和self互相持有了,self->block->self,self等待block释放才能释放,block要想释放需要内部持有的self释放。
线程和runloop之间的关系是怎么样的
线程和runloop是一一对应的
GCD 中串行并行队列,同步异步的区别
串行和并行是说的队列,串行就是按照顺序一个一个执行,并行就是同时并发执行。
同步异步说的是执行函数,同步就是不能开启新线程,只在当前线程执行,异步就是可以开启新的线程,多线程执行。
有遇到过死锁么,怎么产生的
死锁就是队列引起的循环等待,串行队列任务的相互等待。
典型的例子,主队列同步任务。
override func viewDidLoad() {
super.viewDidLoad()
DispatchQueue.main.sync {
print("aaaa")
}
}
在主线程中运用主队列同步,也就是把任务放到了主线程的队列中。
同步对于任务是立刻执行的,那么当把任务放进主队列时,它就会立马执行,只有执行完这个任务, viewDidLoad 才会继续向下执行。
而 viewDidLoad 和任务都是在主队列上的,由于队列的先进先出原则,任务又需等待 viewDidLoad 执行完 毕后才能继续执行,viewDidLoad 和这个任务就形成了相互循环等待,就造成了死锁。
想避免这种死锁,可以将同步改成异步 dispatch_async,或者将 dispatch_get_main_queue 换成其他串行 或并行队列,都可以解决。
runtime查找方法的过程
SEL是方法编号,也就是方法名称,在dyld加载镜像时,通过read_image方法加载到内存的表中IMP是函数实现指针,找IMP就是找函数(方法)的过程
这个流程主要如下:
- 快速查找,通过
objc_msgSend这段汇编代码去我们对应的类的catch_t中去查找,对于调用过的方法会存储在类中的catch中的buckets中。 - 慢速查找,如果在缓存中找不到就要进入慢速查找流程,递归查找自己和父类中的方法,调用
lookUpImpOrForward方法。查找的位置就是class里面的bits->data->ro/rw。- 对象方法,找自己->父类……->NSObject
- 类方法,找自己->父类……->NSObject(根元类)-> NSObject
lookUpImpOrForward首先还是会查找一遍catch,然后找自己,找父类catch,以此类推,直到找到nil,也就是没有父类了,如果找到了,会把找到的放入到对应类的缓存catch中。- 如果找不到消息就进行动态方法解析
resolveInstanceMethod动态解析实例方法resolveClassMethod动态解析类方法- 可以在
NSObject分类实现resolveInstanceMethod方法来达到类方法的决议。isa走位图
- 如果动态方法解析也没有处理就会进入到消息转发流程
- 快速转发
forwardingTargetForSelector,这里直接返回一个能响应该消息的实例即可,返回self会造成死循环 - 慢速转发,消息签名
methodSignatureForSelector,这里只有签名匹配即可,对参数和返回值没有要求 forwardInvocation,是配合methodSignatureForSelector一起使用的,anInvocation通过Runtime保存methodSignatureForSelector提供的签名forwardInvocation方法的实现不仅仅可以转发消息,还可以合并响应各种不同消息的代码,从而避免为每个选择器编写单独方法的麻烦。
- 快速转发
- 最终仍未找到消息:程序crash,报经典错误信息
unrecognized selector sent to instance xxx
runtime 是怎么实现weak置nil的
weak:在被强引用之前,尽可能的保留,不改变引用计数;weak引用是弱引用,你并没有持有它;它本质上是分配一个不被持有的属性,当引用者被销毁(dealloc)时,weak引用的指针会自动被置为nil,可以避免循环引用。
Runtime维护了一张weak表,存储指向某个对象的所有weak指针。weak表其实是一个哈希表。key是所指向对象的地址,Value是weak指针的地址数组(这个地址的值是所指向对象的地址)
weak的实现原理可以概括为三步:
- 初始化时:
Runtime会调用objc_initWeak函数,初始化一个新的weak指针指向对象的地址 - 添加引用时:
objc_initWeak函数会调用objc_storeWeak函数,objc_storeWeak函数的作用是更新指针指向,创建对应的弱引用表。 - 释放时:调用
clearDeallocating函数。clearDeallocating函数首先根据对象地址获取所有weak指针地址数组,然后遍历这个数组把其中的数据设为nil,最后把这个entry从weak表中删除,最后清理对象记录。
关联对象是线程安全的么
是的,因为在AssociationsManager初始化和析构的时候会加锁。
加的锁是spinlock_t。关于spinlock_t可以参考我的其他文章:
using spinlock_t = mutex_tt<LOCKDEBUG>;
class mutex_tt : nocopy_t {
os_unfair_lock mLock;
// 此处省略80多行代码...
}
Replacement for the deprecated OSSpinLock. Does not spin on contention but waits in the kernel to be woken up by an unlock.
译:os_unfair_lock是用来替代OSSpinLock这个自旋锁的互斥锁,不会自旋,在内核中等待被唤醒。所以说spinlock_t并不是如它的名字一般,而是个互斥锁。
isKindOf 和 isMemberOf 区别
+ (BOOL)isMemberOfClass:(Class)cls {
return self->ISA() == cls;
}
- (BOOL)isMemberOfClass:(Class)cls {
return [self class] == cls;
}
+ (BOOL)isKindOfClass:(Class)cls {
for (Class tcls = self->ISA(); tcls; tcls = tcls->getSuperclass()) {
if (tcls == cls) return YES;
}
return NO;
}
- (BOOL)isKindOfClass:(Class)cls {
for (Class tcls = [self class]; tcls; tcls = tcls->getSuperclass()) {
if (tcls == cls) return YES;
}
return NO;
}
测试:
int main(int argc, const char * argv[]) {
@autoreleasepool {
BOOL re1 = [(id)[NSObject class] isKindOfClass:[NSObject class]]; //
BOOL re2 = [(id)[NSObject class] isMemberOfClass:[NSObject class]]; //
BOOL re3 = [(id)[LGPerson class] isKindOfClass:[LGPerson class]]; //
BOOL re4 = [(id)[LGPerson class] isMemberOfClass:[LGPerson class]]; //
NSLog(@"\n re1 :%hhd\n re2 :%hhd\n re3 :%hhd\n re4 :%hhd\n",re1,re2,re3,re4);
BOOL re5 = [(id)[NSObject alloc] isKindOfClass:[NSObject class]]; //
BOOL re6 = [(id)[NSObject alloc] isMemberOfClass:[NSObject class]]; //
BOOL re7 = [(id)[LGPerson alloc] isKindOfClass:[LGPerson class]]; //
BOOL re8 = [(id)[LGPerson alloc] isMemberOfClass:[LGPerson class]]; //
NSLog(@"\n re5 :%hhd\n re6 :%hhd\n re7 :%hhd\n re8 :%hhd\n",re5,re6,re7,re8);
}
return 0;
}
<!--打印结果-->
re1 :1
re2 :0
re3 :0
re4 :0
re5 :1
re6 :1
re7 :1
re8 :1
对于类方法:
isMemberOfClass是判断当前类的元类与传入的类是否一致
isKindOfClass是判断类的元类以及元类的父类一直到根元类和根类是否一致
对于对象方法:
isMemberOfClass是判断当前对象的类与传入的类是否一致
isKindOfClass是判断当前对象的类以及类的父类一直到根类是否一致
16038537231841
iOS Class结构
struct objc_class : objc_object {
// Class ISA;
Class superclass;
cache_t cache; // formerly cache pointer and vtable
class_data_bits_t bits; // class_rw_t * plus custom rr/alloc flags
class_rw_t *data() const {
return bits.data();
}
}
ISA //isa指针 ,继承自objc_object
superclass // 父类指针
cache // cache_t类型的结构体(缓存调用过的方法)
bits // class_data_bits_t结构体(存储类信息,rw,ro)
load 和 initialize 区别
调用方式不同:
- load是根据函数地址直接调用,在
load_images函数中,判断实现了load就会调用call_load_methods(); - initialize是通过发送消息调用的,也就是
objc_msgSend
调用的时机不同: - 子类未实现initialize方法时,会调用父类initialize方法,子类实现initialize方法时,会覆盖父类initialize方法。
- 当有多个Category都实现了initialize方法,会覆盖类中的方法,只执行一个(会执行Compile Sources 列表中最后一个Category 的initialize方法)
- load和initialize方法都会在实例化对象之前调用,load执行在main函数以前,initialize在第一次收到消息的时候,这两个方法会被自动调用,不能手动调用它们。
- load和initialize方法都不用显示的调用父类的方法而是自动调用
- 子类没有initialize方法也会调用父类的方法,而load方法则不会调用父类
- initialize方法对一个类而言只会调用一次(Person、或者是Person+Category)都是一个Perosn类。load方法则是每个都会调用,只要你写了load方法,添加到工程都会实现。
- load方法通常用来进行Method Swizzle,initialize方法一般用于初始化全局变量或静态变量。
- load和initialize方法内部使用了锁,因此它们是线程安全的。实现时要尽可能保持简单,避免阻塞线程,不要再使用锁。
说一下kvo实现的原理,使用kvo需要注意什么,手动触发应该怎么做
我的其他文章iOS Objective-C KVO 详解
由于KVO并没有开源,在官方文档中我们可以看到KVO使用的是一种叫做isa-swizzling的技术。
原本上我们的isa指针指向的是对象所属的类,这个类维护了一个哈希表,这个哈希表实质上包含指向该类实现的方法的指针以及其他数据。
在为对象的属性注册观察者的时候,将修改观察对象的isa指针,指向中间类而不是真实的类,所以isa的值不一定反映的是实例的实际的类,所以我们永远不要依靠isa指针来确定类成员。
我们可以举个例子,在注册观察者前后我们可以查看isa的指向,会从原本的类变成NSKVONotifying_xxx,xxx是类名。这个类可以通过打印原始类的子类打印出来。
KVO监听属性的改变时通过属性的set方法。一般来说,继承于NSObject根类的对象及其属性都自动符合KVO机制。直接给实例变量赋值不会触发KVO。
这个派生类中重写了原始类对于属性的set方法,增加了_isKVOA方法,重写了NSObject的class和dealloc方法,这也就是我们为什么能获取到一个叫做NSKVONotifying_xxx的类,但是打印类名还是原始类,这个就是为了保持一致性。并且该类也需要dealloc所以移除观察者销毁对象的时候也要用到。
当移除观察者后isa就指回了原始类,但是不会销毁中间类,应该是创建一个中间类很耗费资源,所以创建了就保留着,说不上啥时候还会用,等再次使用的时候就不用重新创建了。
1. KVO是苹果提供给开发者的一套键值观察的API
2. KVO由注册观察者,监听通知,移除观察三个步骤组成
3. 有自动观察和手动观察两种模式
4. 对于可变集合需要通过`mutableXXXValueForKey`的相关方法触发更改,比如`mutableArrayValueForKey`
5. 我们还可以注册从属关系的键值观察,KVO支持一对一和一对多两种
6. kvo的本质是`isa-swizzling`技术,通过生成中间类(派生类)来实现属性的观察
7. 中间类会重写属性的se+~~A}~~tter方法以及重写class方法、dealloc方法和_isKVOA方法
调用class方法输出的是原本的类名,为了使用上没有差别。实际是NSKVONotifying_xxx,重写了class方法,所以返回的是原本的类名。
有多个分类实现同一个方法,最后会执行哪个
会执行最后加载的那个。
iOS 产生卡顿的原因,什么是离屏渲染
产生离屏渲染的本质是对过多图层进一些操作不能一次性完成,此时就会触发离屏渲染。
触发离屏渲染的几种情况:
- 使用了
mask的layer(layer.mask) - 需要进行裁剪的
layer(layer.maskToBounds / view.clipsToBounds) - 设置了组透明度为
yes,并且透明度不为1的layer(layer.allowsGroupOpacity/layer.opacity) - 添加了投影的layer(layer.shadow*)
- 采用了光栅化的layer(layer.shouldRasterize)
- 绘制了文字的layer(UILabel,CATextLayer,core Text等)
其实以上并不一定会触发离屏渲染,触发离屏渲染的本质就是多级图层,没办法一次性绘制,需要通过离屏渲染缓冲区分别渲染绘制,最后放到同一个帧缓冲区再去显示。
比如说圆角,并不一定会触发离屏渲染,只有图层多的时候才会,比如同时设置了背景颜色和背景图。
iOS 设备的硬件时钟会发出 Vsync(垂直同步信号),然后 App 的 CPU 会去计算屏幕要显示的内容,之后将 计算好的内容提交到 GPU 去渲染。随后,GPU 将渲染结果提交到帧缓冲区,等到下一个 VSync 到来时将缓 冲区的帧显示到屏幕上。也就是说,一帧的显示是由 CPU 和 GPU 共同决定的。 一般来说,页面滑动流畅是60fps,也就是1s有60帧更新,即每隔16.7ms就要产生一帧画面,而如果CPU 和 GPU 加起来的处理时间超过了 16.7ms,就会造成掉帧甚至卡顿
沙盒文件目录
- Documents 目录:您应该将所有的应用程序数据文件写入到这个目录下。这个目录用于存储用户数据。该路径可通过配置实现iTunes共享文件。可被iTunes备份。希望用户看到的放到这里
- Library 目录:这个目录下有两个子目录:
- Preferences 目录:包含应用程序的偏好设置文件。您不应该直接创建偏好设置文件,而是应该使用NSUserDefaults类来取得和设置应用程序的偏好.
- Caches 目录:用于存放应用程序专用的支持文件,保存应用程序再次启动过程中需要的信息。
- Saved Application State
- SplashBoard
- 可创建子文件夹。可以用来放置您希望被备份但不希望被用户看到的数据。该路径下的文件夹,除Caches以外,都会被iTunes备份。
- tmp 目录:这个目录用于存放临时文件,保存应用程序再次启动过程中不需要的信息。该路径下的文件不会被iTunes备份。有可能会被系统清理掉
- SystemData 系统数据
说一下从点击屏幕开始到某个按钮触发中响应链传递机制,如果要更改响应范围怎么做
触摸事件的传递是从父控件传递到子控件,如果父控件不能接受触摸事件,那么子控件就不可能接收到触摸事件。
- 硬件接收到事件进行传递
- 当iOS程序发生触摸事件后,系统会利用Runloop将事件加入到UIApplication的任务队列中,source1
- UIApplication分发触摸事件到UIWindow,然后UIWindow依次向下分发给UIView
- UIView调用hitTest:withEvent:方法看看自己能否处理事件,以及触摸点是否在自己上面。
- 如果满足条件,就遍历UIView上的子控件。重复上面的动作。
- 直到找到最顶层的一个满足条件(既能处理触摸事件,触摸点又在上面)的子控件,此子控件就是我们需要找到的第一响应者。
事件的传递和响应的区别:
事件的传递是从上到下(父控件到子控件)
事件的响应是从下到上(顺着响应者链条向上传递:子控件到父控件)
更改响应范围可以重写pointInside:withEvent:方法。CGRectContainsPoint(frame, point)
常用的锁有哪些,性能怎么样
NSLock、@synchronized、NSCondition、NSConditionLock、NSRecursiveLock、atomic、dispatch_semaphore、OSSpinLock、pthread_mutex、pthread_rwlock、os_unfair_lock
性能的话参考这篇文章:iOS开发中的11种锁以及性能对比
xcode从开始编译到app出现第一个界面中之间进行了哪些工作(分成xcode编译成功和app启动讲的)
编译分为5个步骤
- Preprocessing 预处理步骤的目的是将你的程序做一些处理然后可提供给编译器。它会处理宏定义、发现依赖关系、解决预处理器指令。
- Compiler 编译器是一个程序,将一种语言的源程序用另一种语言映射到一个语义上等价的目标程序。换句话说,它转换Swift、objective - C和C / C++ 代码到机器码。Xcode 使用两个不同的编译器:一个用于 Swift ,另一个用于Objective - C, Objective - C + +和 C / C++文件。编译过程也会分为好几个步骤,生成不同的中间代码,最后转换成不同架构的机器码。
- clang 是苹果官方的 C 语言编译器。它是开源在:swift-clang。
- swiftc 是 Xcode 用来编译和运行 Swift 源代码的 Swift 编译器。
- Assembler 翻译开发者可读的汇编代码为可重定位的机器码,最终生成包含数据和代码的 Mach-O 文件。
- Linker 链接器将各种对象文件和库链接合并为一个可以在 iOS 或 macOS 系统上运行的 Mach-O 可执行文件。链接器主要有两种文件作为输入,包括这些对象文件的汇编程序和库的几种类型(.dylib, .tbd 和 .a)。
- Loader 最后,加载程序是操作系统的一部分,将一个程序加载到内存中,并运行执行它。加载程序负责分配运行程序内存空间和初始化寄存器所需的初始状态。
接下来就是应用的加载了
可以参考我的其他文章:
iOS 应用加载dyld篇
iOS 应用的加载objc篇
iOS Objective-C 分类的加载
其中dyld的加载主要是环境变量的配置、共享缓存映射、主程序初始化、动态库的加载、主程序的链接、动态库的链接、符号的绑定、运行所有初始方法、进入主程序入口(main)
在初始化方法中会设计到runtime的加载,加载类信息(_objc_init->load_images)
在dyld中书册回调函数,可以理解为添加观察者
在objc中dyld注册,可以理解为发送通知
触发回调,可以理解为执行通知Selector
音视频开发的简单流程
我不会
PCM 数据格式是怎么样构成的
我也不会
常见的音频压缩方式,优缺点
这个我也不会
算法题:链表的反转
class ListNode {
var val: Int
var next: ListNode?
init() {
self.val = 0
self.next = nil
}
init(_ val: Int) {
self.val = val
self.next = nil
}
init(_ val: Int, _ next: ListNode?) {
self.val = val
self.next = next
}
}
class LinkList {
// 单链表头插法
static func createListHead(node: ListNode, count: Int) {
for i in 1...count {
let n = ListNode(i, node.next)
node.next = n
}
}
// 单链表尾插法
static func createListTail(node: ListNode, count: Int) {
var tmp = node
for i in 1...count {
let n = ListNode(i)
tmp.next = n
tmp = n
}
}
// 链表翻转(就地反转)
static func rotateList(node: ListNode) {
let prev = node.next
var pCur = prev?.next
while pCur != nil {
prev?.next = pCur?.next
pCur?.next = node.next
node.next = pCur
pCur = prev?.next
}
}
// 链表翻转(返回新链表,头插法)
static func rotateList2(node: ListNode) -> ListNode {
var pCur = node.next
let newNode = ListNode()
while pCur != nil {
let n = pCur?.next
pCur?.next = newNode.next
newNode.next = pCur
pCur = n
}
return newNode
}
static func printList(node: ListNode) {
var n:ListNode? = node
while n?.next != nil {
n = n?.next
print(n!.val)
}
}
}
有什么要问的么?