iOS开发几大问题

401 阅读20分钟

一、内存管理

1、引用计数

引用计数

  • OC类中实现了引用计数器,对象知道自己当前被引用的次数。
  • 对象初始化时计数器为1,每次操作对象都会引起相应的计数器变化。
  • retain + 1, release -1
  • 当对象的引用计数为0时,给对象发送dealloc消息销毁对象。

黄金法则

  • 凡是通过alloc、init、copy、mutableCopy、retain进行创建的对象,都要使用release或autorelease进行释放。
  • 自己生成的对象,自己持有。
  • 不是自己生成的对象,自己也能持有。
  • 不再需要持有对象时释放。
  • 非自己持有的对象无需释放。

引用计数存放

  • 从64bit开始,对象的引用计数存放在优化过的isa指针中,也可能存放在SideTable中。
  • 当优化过的isa指针中,引用计数过大存放不下时,就会将引用计数存放到SideTable中。
  • SideTable其实是一个哈希表,key为对象指针,Value为对象内容具体存放的SideTable。
  • SideTable包含自旋锁,引用计数表,弱引用表,由Runtime维护。
  • 为什么是SideTables?:查找或修改引用计数时要加锁的,方便多个对象同时操作。

2、MRC

  • iOS5之前,需要开发者手动去管理内存。
  • 需要引用对象时,发送retain消息,对象的引用计数+1。
  • 不需要引用对象时,发送release消息,对象的引用计数-1。
  • 当引用计数为0时,自动调用对象的dealloc方法销毁对象,释放内存。
  • 引用计数为0的对象,不能再使用release和其他方法。

3、ARC

  • ARC也是基于引用计数,只是编译器在编译时期自动在已有代码中插入合适的内存管理代码(包括retain、release、copy、autorelease、autoreleasepool)以及在Runtime做一些优化。
  • 简单来说,就是代码中自动加入了retain、release,原先MRC中需要手动添加的用来管理引用计数的代码都由编译器帮我们完成了。

4、strong、weak、assign

strong

强引用,指向一个对象时,对象引用计数+1,当对象没有一个强引用指向它时,它才会被释放,如果在申明引用时不加修饰符,默认为Strong。

weak

  • 弱引用,不会引起对象的引用计数变化,当对象被释放时,所有指向它的弱引用指针会自动置为nil,防止野指针。给nil发送消息时,会直接return,不会调用方法,也不会crash。
  • 对象的SideTable中有一个weak表,以对象的内存地址为key,value则是所有指向该对象的弱引用指针的数组。
  • 当对象销毁时,通过对象内存地址找到所有指向它的弱引用指针,置nil并删除。

assign

  • assign指针纯粹指向对象,不会引起对象的引用计数发生变化,当对象被释放时,指针依然指向对象的内存地址,不会自动被置为nil,容易造成野指针。所以一般assign都用来修饰基本数据类型,如:int、float、struct等值类型。
  • 值类型会被放入栈中,遵循先进后出的原则,有系统管理栈内存。
  • 引用类型会被放入堆中,需要我们自己手动管理内存(MRC)或通过ARC管理。

5、深copy与浅copy

深拷贝:copy出来的对象与源对象地址不一致,开辟新的内存空间存放拷贝对象。对拷贝对象修改不会影响源对象。

浅拷贝:copy出来的对象与源对象地址一致,对拷贝对象修改会影响源对象。

对于不可变类型的对象,copy为浅拷贝,对象的引用计数+1. 对于可变类型的对象,copy为深拷贝,拷贝对象也变成不可变类型。 对象做mutableCopy操作,都为深拷贝,拷贝对象也会变为可变类型。 对于容器类型(NSArray、NSDictionary等),深拷贝也紧是拷贝容器本身,对容器里面的元素只做浅拷贝。

声明NSString类型的属性,用copy还是strong修饰更好?

考虑多态的原因,NSString类型的属性,最终可能指向NSMutableString,为了防止源字符串的修改引起变化,最好是采用copy来修饰。

如何自定义copy操作?

遵循copy协议<NSCopying,NSMutableCopying>,重写copyWithZone,mutableCopyWithZone方法。

6、 atomic与nonatomic

  • 对atomic修饰的属性的setter、getter方法添加了原子锁,保证get、set操作的完整性。因为atomic添加了原子锁,会增加开销,运行速度更慢,在不需要保证get、set操作的完整性的情况下,所以一般都使用nonatomic。

atomic不保证线程安全,只能保证set、get的完整性,当开启多个线程执行多个set、get时,无法保证执行的顺序

7、内存泄漏、内存溢出

  • 内存泄漏:是指申请内存空间用完之后没有释放,在ARC下根本原因就是循环引用(在view Controller中没有正确使用NSTimer、delegate、block)引起的。
  • 内存溢出:通俗讲就是内存不够了,程序在申请内存空间时,没有足够的内存空间可供使用。

8、循环引用

两个对象之间互相强引用,引用计数都依赖于对方,导致对象无法释放。 最容易产生循环引用的两种情况就是delegate和block,所以才引入弱引用。持有对象,但不增加引用计数,这样就避免了循环引用的产生。

二、Runtime

OC是一门动态语言

动态语言是指程序在运行时可以改变其结构;添加新的函数、属性,删除已有的函数、属性等结构上的变化,在运行时做类型的检查。

编译时:源代码被编译成机器码的过程

运行时:用户运行编译过的程序的过程,程序运行的过程。

OC的动态性由runtime支持的。

runtime是一个C语言的库,提供API创建类,添加方法,删除方法,交换方法等。

1、id、instance

id 使用id修饰的对象是动态类型,只是简单的声明了指向对象的指针。 编译时不做类型检查,可以发送任何消息给id类型对象。 instancetype 表示某个方法返回未知类型的OC对象 非关联类型的方法返回所在类的类型

区别:instancetype 可以返回和方法所在类相同类型的对象,id 只能返回未知的类型对象 instancetype 只能作为返回值,不能像id一样作为参数 关联返回类型

  • 类方法中,以alloc、new开头
  • 实例方法中,以autorelease、 init、retain、或self开头。

当方法的返回值为id类型,方法不会返回一个类型不明的对象,会返回一个方法所在类类型的对象。

非关联返回类型

  • 类方法中,不以alloc、new开头
  • 实例方法中,不以autorelease、init、retain、或self开头

当方法的返回值为id,方法会返回一个类型不明的对象;可以用instancetype作为方法的返回值的类型,返回一个方法所在类类型的对象;

NSObject *

  • 声明类指向NSObject类型对象的指针,编译时要做类型检查
  • NSObject是OC中的基类,绝大多数类都继承于NSObject

id < NSObject >

  • 也是一个指针,要求指向的类型要实现NSObject protocol
  • NSObject、NsProxy类实现了NSObject接口,id< NSObject >可以指向它们;

OC对象的本质

  • OC对象本身是一个结构体,这个结构体只有一个isa指针
  • 任何数据结构,只要在恰当的位置有一个指针指向一个class,那么它就可以被认为是一个对象。

NSObject对象的内存大小

  • 64bit下,bool, signed char、unsigned char 占1个字节;
  • short、unsigned short占2个字节, int、unsigned int、float占4个字节;
  • long、unsigned long、long long、 double占8个字节。
  • NSObject占8个字节
  • 结构体内成员按自身长度自对齐
  • 对象内存申请的时候按8字节对齐,开辟内存时按16字节对齐;

2、isa(is a what?)

objc_object *id;
struct objc_object {
    Class isa;
}
objc_class *Class;
struct objc_class {
    super_class,name,version,info,instance_size,ivars,methodLists,cache,protocols
}
isa指向流程
  • 实例对象isa指向类对象
  • 类对象isa指向元类
  • 类对象superClass指向父类的类对象
  • 所有元类isa指向NSObject对象的元类(根元类)
  • 根元类isa指向自己
  • 根元类的superClass指向NSObject的类对象
  • 元类的superClass指向对应父类的元类
category
objc_category *Category;
struct objc_category {
    category_name,class_name,instance_methods,class_methods,protocols
}
  • 在程序运行时
  • 实例方法整合到主类中
  • 类方法整合到元类中
  • 协议同时整合到主类和元类中
category延伸

1、在类的+load方法中可以调用category里声明的方法吗?

可以,因为附加Category到类的工作先于+load方法的执行

2、类和category的+load方法的调用顺序

先类,后category。而各个category的+load方法按照编译的顺序执行

3、关联对象存在哪?

所有关联对象都由AssociationsManager管理,AssociationsManager里面有一个静态AssociationsHashMap来存储所有关联对象的。

4、在category里面可以添加属性吗?

  • category中只能添加方法,不能添加实例变量。类的内存大小是在编译时确定,而category是在运行时被添加的,此时再添加实例变量会破坏内存结构。

  • 在category中添加属性,通过关联对象实现setter、getter方法。 5、类的category的同名方法调用顺序

  • category并不是完全替换掉主类的同名方法,只是类的方法中会出现名字一样的方法且category的方法会排在前面,多个category中的同名方法按编译顺序排。

  • runtime查找方法按照顺序,一旦找到就return。

  • 便利类的方法列表,列表里最后一个同名的方法,即是原方法。

category与extension

category

1、运行时决议

2、有单独的.h和.m文件

3、可以为系统类添加分类

4、看不到源码的类添加分类

5、职能添加方法,不能添加实例变量

extesion

1、编译时决议

2、以声明的方式存在,寄生于主类.m文件

3、不可以为系统类添加extension

4、没有.m源码的类不可以extension

5、可以添加方法,可以添加实例变量,默认为@private

3、消息发送机制

在OC中对象调用方法其实是对象接收消息,消息的发送采用“动态绑定”的机制,具体调用哪个方法直到运行时才能确定,确定后才回去执行绑定的代码

OC对象调用方法在运行时会被转化为

//SEL:方法名
//IMP:指向方法实现的函数指针
void objc_msgSend(id self, SEL, cmd...)

1、根据消息接收者的isa确定自己所属的类,先在类的cache和MethodLists中从上向下查找IMP;

2、如果本类中没有找到,则会根据本类的superClass指针,沿着继承体继续向上查找(向父类查找);

3、如果向父类查找都没有找到,则会进入消息转发流程。

消息转发流程

1、动态解析

+(BOOL)resolveInstanceMethod:(SEL)selector;
+(BOOL)resolveClassMethod:(SEL)selector;

2、备用接收者

-(id)forwardingTargetForSelector:(SEL)selector;

3、消息重定向

-(void)forwardInvocation:(NSInvocation *)anInvocation;
-(NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector;

4、最后消息未能处理的时候,还会调用

-(void)doseNotRecognizeSelector:(SEL)aSelector抛出异常

Method swizzling

4、Method Swizzing(方法交换)

5、Category、Extension

6、归档解档(NSCoding)

7、KVO、KVC

KVC

是一种可以通过Key来访问类属性的机制,而不是通过调用setter、getter方法访问。可以在运行时动态访问和修改对象的属性。

//赋值 setvalue forkey //取值 valueforkey

forkeypath是对更深层次的对象进行访问,如数组的某个元素,对象的某个属性。

setvalue forkey

  • 按照setkey、_setKey的顺序查找方法,找到了就传递参数,调用方法。
  • 如果没有找到,则查看accessInstanceVariableDirectly方法的返回值,如果为No(默认YES)就不再继续往下执行,直接调用setValue:forUndefineKey抛出NSUnkonwKeyException异常。
  • 如果返回为YES,则按照_key, _isKey, key, isKey的顺序查找成员变量,找到了就直接赋值
  • 如果没有找到,则调用setValue:forUndefineKey跑出异常。

value:forKey

  • 按getkey、_getKey的顺序查找方法,找到了就传递参数,调用方法。
  • 如果没有找到,则查看accessInstanceVariableDirectly方法的返回值,如果为No(默认YES)就不再继续往下执行,直接调用Value:forUndefineKey抛出NSUnkonwKeyException异常。
  • 如果返回为YES,则按照_key, _isKey, key, isKey的顺序查找成员变量,找到了就直接赋值
  • 如果没有找到,则调用Value:forUndefineKey跑出异常。

KVO

KVO:key value observing,键值监听,可以监听对象的某个属性的变化

  • 给对象添加监听
  • 通过runtime动态创建一个子类,修改对象的isa指向子类
  • 子类重写set方法,内部执行顺序 villChangeValueForKey

【super setKey】

didChangeValueForKey

在didChangeValueForKey中调用KVO的回调方法:

observeValueForKeyPath:ofObject:change:context:

isa-swizzling在KVO中是怎样实现的?

  • addObserver:forKeyPath:
  • 给A添加观察后,系统会自动创建NSKVONotifying_A这样一个类
  • 改写isa指针的指向,
  • NSKVONotifying_A继承自A,并重写setter方法,重写的setter方法负责通知所有观察者

三、RunLoop

1、什么是RunLoop?

2、RunLoop Mode

3、RunLoop Source

4、RunLoop Observer

5、RunLoop运行逻辑

6、如何实现常驻线程

四、Block

1、block原理

block是一个结构体,通常包涵两个成员变量:__block_impl 、__block_desc和一个构造函数。

__block_impl {
    void *isa;//isa指针,指向一个类对象,有三种类型;
    int Flags;//负载信息(引用计数和类型信息),按位存储
    void *FuncPtr;//指向block执行时的函数指针
}

1、通过block结构体和构造方法生成一个实例,并用一个指针指向当前实例

2、通过block->FuncPtr找到要执行的函数指针

2、堆block

NSMallocBlock堆block,栈block调用了copy(在ARC下访问了auto变量且有强引用指向该block或做为函数的返回值,就会自动将栈block copy到堆上),全局block调用copy还是全局block,堆block调用copy引用计数+1;

3、栈block

NSStaticBlock栈block,访问了外部auto变量(在ARC下没有强引用指向这个block,而是直接打印出来)

4、全局block

NSGlobalBlock全局block,没有访问外部auto变量(我们平时写的局部变量,默认就有auto,自动变量,离开作用域就销毁),访问外部static或者全局变量还是全局block

5、变量捕获

<1>.局部变量为什么要捕获?

考虑作用域的问题,在block里属于垮函数来访问局部变量,所以需要捕获

<2>.auto变量值传递,static变量指针传递

auto变量可能会销毁,内存可能会消失,不采用指针访问,static变量一直保存在内存中,采用指针访问

<3>.全局变量不需要捕获,直接访问

<4>.block里访问self会捕获self;OC函数转成C++时,self和_cmd作为函数默认传的参数,是局部变量,所以要捕获。

<5>.block里面访问成员变量先是捕获self,然后根据self访问成员变量

6、__block 、__weak

_ _block不管是MAC还是ARC下都可以使用,可以修饰对象,也可以修饰基本数据类型。 _ _weak只能在ARC下使用,只能修饰对象,不能修饰基本数据类型。

1、解决循环引用

  • 在ARC下使用__weak,在MRC下使用__block. 2、修改变量
  • _ _block 修饰的变量在block中保存的是变量的地址,使用__block修饰之后的变量类似于static变量。
  • _ _block不能修饰全局变量、静态变量(static)

7、weakSelf

如果block在栈空间,不管外部变量是强引用还是弱引用,block都会弱引用访问对象; 如果block在堆空间,如果外部强引用,block内部也是强引用,如果外部弱引用,block内部也是弱引用。 _ _weak typedof(self)weakSelf = self; 1、block是controller的属性,如果内部没有使用weakSelf会造成内存泄漏; 2、block不是controller的属性,内部使用self不会造成内存泄漏; 3、当使用类方法有block作为参数使用时,block内部使用self不会造成内存泄漏;

五、多线程

什么是多线程

进程、线程

  • 进程:当一个程序进入内存运行,即变成一个进程,进程是处于运行过程中的程序。并且具有一定的独立功能。
  • 线程:线程是进程中的一个执行单元,负责当前进程中程序的执行,一个进程至少有一个线程,一个进程中可有多个线程。
  • 单线程程序:若有多个任务只能一次进行。
  • 多线程程序:若有多个任务,可以同时执行。 对于CPU的一个核心而言,某个时刻只能执行一个线程,而CPU在多个线程之间切换的速度相对于我们的感觉要快,看上去就是在同一时刻执行。多线程并不能提高程序的执行速度,但能提高运行效率

任务

  • 任务:执行操作的意思,就是要在线程中执行的代码。 同步执行(sync)
  • 同步添加任务到队列中,队列在任务执行结束之前会一直等待,知道任务完成之后再继续执行。
  • 只能在当前线程中执行任务,不具备开启线程的能力。 异步执行(async)
  • 异步添加任务到队列中,队列不会等待,可以继续执行其他任务。
  • 可以在新的线程中执行任务,具备开启线程的能力,但不一定开启新线程。 队列
  • 队列(dispatch queue):执行任务的等待队列,即用来存放任务的队列,队列是一种特殊的线性表,采用FIFO(first in first out)的原则。新的任务总是被插到队列的末尾,读取任务总是从队列的头部开始读区,则从队列中释放一个任务。
  • 主队列:是一个特殊的串行队列,添加到主队列的任务只能在主线程中执行。

任务 + 队列

  • 同步执行 + 并发队列:不开启新线程,串行执行任务。
  • 同步执行 + 串行队列:不开启新线程,串行执行任务。
  • 同步执行 + 主队列:线程锁死。
  • 异步执行 + 并发队列:开启新线程,并发执行任务。
  • 异步执行 + 串行队列:开启一条新线程,串行执行任务。
  • 异步执行 + 主队列:不开启新线程,串行执行任务。

队列 + 任务 + 任务

  • 并发队列 + 异步执行 + 同步执行:不开启新的线程,串行执行任务。
  • 并发队列 + 异步执行 + 异步执行:开启新线程,并发执行任务。
  • 并发队列 + 同步执行 + 同步执行:不开启新线程,串行执行任务。
  • 并发队列 + 同步执行 + 异步执行:开启新线程,并发执行任务。
  • 串行队列 + 异步执行 + 同步执行:线程死锁。
  • 串行队列 + 异步执行 + 异步执行:开启一条新线程,串行执行任务。
  • 串行队列 + 同步执行 + 同步执行:线程死锁。
  • 串行队列 + 同步执行 + 异步执行:开启一条新线程,串行执行任务。

串行队列

  • 串行队列(serial dispatch queue):只开启一个线程,每次只能执行一次任务,一个任务执行完毕后才能执行下一个任务。 并发队列
  • 并发队列(concurrent dispatch queue):可以让多个任务并发(同时)执行,可以开启多个线程,并同时执行任务。并行队列的并发功能只能在异步下才有效。 线程安全 在多线程中运行得到的结果与在单线程中运行得到的结果一致,即为线程安全。

GCD信号量

保持线程同步,将异步执行转换为同步执行。

保证线程安全,为线程加锁

自旋锁:如果资源被占用,等待的线程会以死循环的方式一直处于忙等状态,一旦资源释放,立马执行。

互斥锁:如果资源被占用,等待的线程会进入休眠状态,直到等待的资源被解锁才被唤醒。 GCD

  • dispatch_barrier_async:栅栏函数,等待前面加到并发队列中的任务全部执行完毕之后,再将指定的任务追加到改异步队列中。
  • dispatch_after:延时执行,不是在指定时间之后才开始执行任务,而是在指定时间之后追加任务到主队列中,严格来讲,只跟时间并不是绝对准确的。
  • dispatch_once:只执行一次,常用于创建单利,在多线程的环境下,也能保证线程安全。
  • dispatch_apply:快速迭代,可以在多线程中同时(异步)便利。
  • dispatch_group_notify:监听group中任务的完成状态,当所有的任务都执行完毕后,追加任务到group中并执行。
  • dispatch_group_wait:阻塞当前线程,等待指定的group中的任务执行完成后,才继续往下执行。
  • dispatch_group_enter:表示一个任务追加到group中。
  • dispatch_group_leave:表示一个任务离开group。
  • dispatch_semaphore:信号量
  • dispatch_semaphore_cteate:创建信号量,并初始化信号总量。
  • dispatch_semaphore_signal:信号量+1;
  • dispatch_semaphore_wait:信号量-1;

信号量<0 时会一直阻塞所在线程,反之就可以正常执行。

NSOperation、NSOperationQueue

NSOperation、NSOperationQueue是基于GCD更高一层的封装。

  • 可添加完整的代码块,在操作完成后执行
  • 添加操作之间的依赖关系,方便控制执行顺序
  • 设定操作的优先级
  • 可以很方便的取消一个操作
  • 可以使用KVO观察操作执行状态的变更:isExecuting、isFinished、isCanceled NSOperation

NSOperation:操作,即GCD中的任务,将要在线程中执行的代码片段 NSOperationQueue:操作队列,不同于GCD中的队列FIFO的原则。对于添加到队列中的操作,首先进入准备就绪的状态(就绪状态取决于操作之间的依赖关系),然后就绪状态的操作的开始执行顺序由操作之间的优先级决定。

没有依赖关系的操作先进入就绪状态,根据优先级决定执行顺序;当前操作依赖的操作执行完毕,当前操作进入就绪状态。

  • 通过设置最大并发操作数来控制并发和串行
  • 默认为-1,不做限制,可进行并发执行;==1是串行队列,串行执行;>1是并发队列,并发执行
  • 被添加到队列的操作,默认是异步执行的
  • 主队列运行在主线程
  • 自定义队列同时包含串行、并发的功能,运行在其他线程
  • 通过设置队列的isSuspended属性,可实现队列的暂停与继续的效果,正在执行的操作不受影响
  • 可以取消队列中所有的操作,也可取消单个操作,只对未执行的操作有效,操作已经在执行中,系统不会强制停止这个操作,只会标记cancelled属性为true。

六、性能优化

1、卡顿优化

2、耗电优化

3、启动优化