- 怎样将oc代码反编译成C和C++代码?
使用xcode内置的LLVM的前端编译器clang,这样生成的代码并不完全是底层实现,只是一个参考
命令:clang -rewrite-objc 文件名称.m -o 输出文件名称.cpp
指定平台命令:xcrun -sdk iphoneos clang -arch arm64 -rewrite-objc oc源文件.m -o 目标输出文件.cpp
如果需要连接其他框架,-framework 框架名(UIKit)
添加arc支持 -fobjc-arc -fobjc-runtime=ios-8.0
- 获取oc源码
从官网下载,地址:https://opensource.apple.com/tarballs/objc4
- 在xcode实时查看内存数据
Debug -> Debug workflow -> View Memory
也可以使用LLDB指令,也就是xcode的控制台
- 常用的LLDB指令
1. p <====> 打印
2. po <===> 打印对象
3. memory read/数据格式字节数 内存地址 <===> 读取内存
4. x/数量(几段) 格式(进制数 x=16进制 f=浮点 d=10进制) 字节数(b=1 h=2 w=4 g=8) 内存地址 <===> 读取内存
5. memory write 内存地址 数值 <===> 修改内存中的值
、
6. step 单步执行OC代码
7. stepi == si 单步执行汇编代码
8. continue 继续执行,跳到下一个断点
9. next 单个函数执行
- 在控制台通过函数体地址获取函数信息
p (IMP)地址
- LLVM中间代码的生成
OC代码经过LLVM经过编译之后会先生成跨平台的中间代码,然后再生成汇编代码及二进制
生成中间的代码的命令是:clang -emit-llvm -S 文件名
这个中间代码是一种LLVM独有的语言,官方文档:https://llvm.org/docs/LangRef.html
- 在调试过程中控制台查看所有的调用栈
控制台输入命令:bt
- iOS Fundation框架源码
GUNStep计划将OC的库从新实现一遍并且进行开源,源码接近于苹果的源码
源码地址:https://www.gunstep.org/resources/downloads.php
- GCD源码
https://github.com/apple/swift-corelibs-libdispatach
-
OC对象的本质
- OC中类和对象都是基于C和C++的结构体实现的;
- OC中的对象分为三种:实例对象、类对象、元类对象
实例对象的内存分配情况
- OC中的基类就是NSObject,这个纯洁的实例对象实际上只有一个成员变量isa,isa是一个指针,在64位系统下占用8个字节。然而NSObject在实例化后他所分配和占用的空间是16字节,在源码中有注释说明一个oc对象的最小占用16个字节;
其实OC在对象的内存分配和占用上做了优化,学名叫'字节对齐'。在占用内存上它规定结构体的大小必须是最大成员大小的倍数。在分配内存上至少是16或者16的倍数
。
CPU读取内存的时候分为大端和小端模式,iOS是使用的小端模式,也就是从高位开始读
-
实例对象
就是通过alloc分配内存生成的对象,内部结构是一个isa指针和它的成员变量的值; -
类对象
在内存中有且只有一个,通过类和实例都能获取到类对象。其中包括isa指针,superClass指针,类的属性信息,类的对象方法信息,类的协议信息、类的成员变量信息; -
元类对象
在每个类的内存中也只有一个,通过runtime方法object_getClass(类对象)获取。内部结构跟类对象一样都是Class类型,只是用途不一样。其中包括:isa指针,superclass指针,类的类方法信息;
-
isa指针和superclass指针
- 实例对象的isa指向类对象,类对象的isa指向元类对象,元类对象的isa指向基元类,
基元类的isa则指向自己
; - 实例对象中没有superclass指针,只有类对象和元类对象中有,superclass就是指向父类,
根元类对象的superclass指向根类对象,根类对象的superclass指向nil
; - 在64位架构以前实例对象的isa地址值直接就是类对象的地址值,在arm64架构之后需要用实例对象的isa的地址值 & ISA_MASK(arm64 == 0x0000000ffffffff8 x_86_64 == 0x00007fffffffff8)才能获取到真正的类对象的地址值。 类对象同样如此;
- 目前OC最新的版本是2.0,在源码中的实现也进行了更改,其中最大的变化是对象的结构体实现使用的是c++的方式实现的(其中FASK_DATA_MASK == 0x00007ffffffffff8);
-
如果想要窥探到类对象中的结构,需要仿照源码写一套同样的结构的结构体出来(类实际上就是结构体),然后对类对象进行强转;
截屏2021-07-25 上午10.09.40.png
-
KVO
- Key Value Observing 健值监听技术,可以对任意对象的属性进行监听;
- 当一个对象A的属性被监听的后,程序运行中会动态生成一个NSKVONotifying_A并且继承自A类的子类 。同时对象A的isa指针会指向这个子类NSKVONotifying_A。在调用A对象属性的set方法时,会通过isa指针找到类对象NSKVONotifying_A的set方法。在该set方法中时实现大概是先调用willChangeValue 方法,再调用super的set方法,最后再调用didChangeValue方法并且调用observer的observerValueForKeypath方法实现监听;
- 在NSKVONotifying_A类中还实现了class、dealloc、isKVOA方法;
- 如果想要手动触发kvo,可以先调用对象的willChangeValueForKey方法,再调用didChangeValueForkey方法;
-
KVC
-
Key Value Coding 健值编码技术,通过key获取或设置对象的属性的值;
-
setValue:forKey:调用流程;
假如key == name- 先去查找setName的方法实现进行赋值;
- 如果没有找到setName方法的实现,就会调用对象的accessInstanceVariablesDirectly方法,查询是否能够直接访问方法变量;
- 如果是No,直接抛出异常。
- 如果是Yes,会依次对name, isName, _name, _isName的变量进行赋值,如果这个几个变量都没有也会抛出异常;
如果赋值成功,会触发KVO
-
getValueForKey:调用流程;
- 先调用getName方法获取值;
- 如果没有getName的方法实现,就会访问对象的accessInstanceVariablesDirectly方法确认是否能直接访问变量;
- 如果是NO,直接抛出异常。
- 如果是YES,依次访问成员变量_name, _isName, name, isName的值
-
Category
-
所有的分类在程序编译时,会被包装成category_t结构体,里面存放着,类名,class指针,对象方法和类方法列表,属性列表,协议列表;
-
在程序运行的时候,会将所有分类的数据合并到类对象中去。比如分类中的方法列表;
- 倒序遍历分类数组,取出分类中的方法列表。
- 类对象中的方法列表是一个二维数组进行维护的。将二维数组扩容,再将类对象中的方法列表移到数组最后,在所有分类中的方法列表生成的二维数组,插到类对象的方法二维数组前面。
-
所以如果分类中重写了类对象的方法,会优先调用分类中的方法;
-
在程序启动的时候,不管类或者分类有没有被使用,都会加载compile sources列表中类和分类的load方法。调用顺序会根据xcode中compile sources列表顺序优先调用所有类的load,并且先调用父类。再根据compile sources列表顺序加载调用所有的分类的load。
在调用load方法的时候,使用的函数地址直接调用,并没有使用消息发送机制,所以每个类和分类的load都会调用
; -
initialize方法只有在类第一次接收消息的时候只会调用一次,也就是使用objc_messageSend方式,并且会优先调用父类的initialize方法。
如果子类中没有实现initailize,就会调用父类的initialize,因为走得是消息发送机制,所以在这种情况下,父类的initialize会被调用多次。
关联对象
-
为分类属性提供储存属性值的地方,通过<objc/runtime.h>api的objc_setAssociatedObject和objc_getAssociatedObject保存和获取属性值。 其中的key最好的方案是使用该属性的get方法,易懂,方便,唯一;
_cmd 表示当前方法的selector
-
关联对象实现原理:它是由AssociationsManager、AssociationsHasMap、ObjectAssociationMap、ObjectAssociation协作完成的。跟原来的类对象不发生关系。如果给A对象分类的属性name赋值,流程如下:
- 通过AssociationsManager拿到AssociationsHasMap;
- 通过对对象A的地址哈希后的值从AssociationsHasMap里面找出ObjectAssociationMap;
- 通过objc_setAssociatedObjec方法中传入的key,在ObjectAssociationMap中找出ObjectAssociation;
-
最后将objc_setAssociatedObjec传入的value和policy存入ObjectAssociation;
image.png
-
-
Block
-
Block本质上也是一个oc对象,主要封装了函数调用及函数调用的环境。主要信息有:isa指针,函数地址,block的描述如Block的size等,捕获的外部的auto局部变量;
-
在引用外部变量的时候不同的情况有不同的捕获机制,只要是auto局部变量就会被捕获;
image.png
auto 修饰的局部变量是指会自动销毁的变量。C语言中定义的局部变量默认就是auto修饰的。
在oc函数中都会隐式的传入self和_cmd两个参数。所以如果block中引用了self或者是self的变量,也会被当做局部变量进行捕获。
- block有三种类型,分别是:
- 全局block(NSGlobalBlock存放在数据区);
- 堆block(NSMallocBlock存放在堆区,程序员自己管理);
- 栈block(NSStackBlock存放在栈区,自动销毁);
他们最终都是继承自NSBlock,block中的isa就指向这些类对象。
block类型 生成条件 copy操作 NSGlobalBlock 内部没有访问auto变量 什么也不做依然是global类型 NSStackBlock 内部访问了auto变量 在arc下依然会变成malloc类型
变成NSMallocBlock NSMallocBlock NSStackBlock 调用copy 引用计数增加 - 在ARC环境时,block在某些特定的环境下,栈区block会自动进行copy操作变成堆区block。如;
- block作为函数返回值时
- 使用强指针引用时
- 使用usingBlock时,比如数组的排序方法
- GCD的block
- 栈区block在内部引用了auto的对象变量时,不管该对象是strong修饰还是weak修饰都不会进行强引用。但是当栈区block从栈区拷贝到堆区时,在block内部会自动根据对象的修饰符进行copy操作,如果是strong被引用对象的引用计数+1,如果是weak,什么都不做。当堆区block在销毁也会被引用auto对象变量进行dispose操作,被引用对象引用计数-1。
- __block可以用来修饰auto变量,以达到可以在block内部修改变量的值。
- 假如__block修饰的auto变量A,在block内部会被包装成一个对象(暂且叫block_A),这个对象内部有:isa、forwarding指针、size、变量A,
如果A是个对象时,还会多一个copy和dispose的函数指针(跟外面那层的copy和dispose功能类似)
。其中forwarding指针是指向它自己的,block_a中的A就是外面__block修饰的变量A。当这个栈block拷贝到堆区的时候,block_a中的forwarding指针就会指向堆区block的block_a,这样就能保证不管在栈区还是堆区,访问的对象都是同一个变量A。
- 在MRC环境下,block内部对__block修饰的对象类型不会自动进行强引用。
- 解决由于block引用外部对象变量时,产生的循环引用,造成内存泄漏的问题:
- 使用__weak修饰外部对象变量。__weak还有一个好处——weak引用的对象销毁后,指针会设置为nil;
- 使用__unsafe__unretained修饰外部对象变量。这种方案可能导致野指针错误,因为对象销毁后指针还是会指向对象所在的地址;
- 使用__block修饰外部对象变量,并且在block内部将对象设置为nil,而且还必须调用这个block,才会释放所有对象。
但是在MRC环境下就是跟__unsafe__retained的效果一样了
;
- 说明一下__weak修饰了外部对象变量以后,为什么需要再block内部再对对象变量__strong重新去修饰一下,之前我也是不太理解。
首先,__strong是为了保证在整个block内部生命周期内,对象变量不会被销毁;再者,__weak修饰的对象变量如果直接访问成员变量,编译器也会报错,因为对象可能随时会销毁。那调用方法为什么不报错呢?是因为OC支持nil可以调用任何方法。
- block有三种类型,分别是:
-
-
Runtime
- Object-C是一门动态性很强的语言,内部就是基于runtime实现了动态性支持;
isa指针
- 在arm64架构之前,isa就是一个普通的指针,直接存储着类对象和元类的地址。在arm64之后,isa被优化了,使用
位域
存储了更多的信息,成了一个union共用体。
image.png
- union是共用体,顾名思义,就是内存共用的意思。其中struct没有实际作用,只是相当于一个隐式注释,表明在bits中存储这些东西并且注明了占用的位数。 位域就是表示占用了多少位。
1. nonpointer——地址中是否包含了其他信息
2. has_assoc——是否包含了关联对象
3. has_cxx_dtor——是否有c++的析构函数
4. shiftclas——class指针地址
5. magic——在调试时,对象是否未完成初始化
6. weakly_referenced——是否有被弱引用指向过
7. deallocating——是否正在释放
8. has_sidetable_rc——当extra_rc存放的引用计数放不下时,是否将引用计数放到sidetable中
9. extra_rc——存放对象的引用计数数值
回顾一下位运算中的与、或、反码,左移的用法;
1. 与 符号 = & 两位同时为“1”,结果才为“1”,否则为0
#假如想取出一个二进制数中的第3位数值
int a = 1;
#如果result > 0 就代表第三位是1
int result = a & (1 << 3)
2. 或 符号 = | 两位只要有一个为1,其值为1,否则为0
#如果想改变某一位的值 就用或
a = a | (1 << 3);
3. 反码 符号 = ~ 取相反的值 如果0b0100 结果:0b1011
4. 左移 符号 = << 将数值以二进制的方式向左移,如0001<<4,结果:1000
-
类对象中的属性协议等信息是由class_rw_t管理的,但是在类初始化的时候会将类的初始化信息放到class_ro_t中保存的,并且其中存放的属性方法协议等信息都是只读的。类信息初始化完之后再将class_ro_t交给class_rw_t管理。不同的是class_rw_t的是信息是可以修改的。
-
method_t是函数咋runtime的封装,里面包含了函数名SEL、返回值类型和方法类型types、函数体指针IMP。
SEL其实是跟char*差不多的东西,可以通过Selector和sel_registerName获取;
types存放了函数返回值和参数的类型编码字符串。可以使用@encode查看具体类型的具体编码。char *types = ‘i24@0:8i16f20’ 1. 第一个i 表示返回值是一个int类型, 占用4个字节; 2. 24 表示函数的参数总大小; 3. @ 表示id类型,占用8个字节,每个函数都会默认传入两个参数slef和_cmd; 4. 0 表示self是从0位开始; 5. : 表示方法_cmd,占用8个字节; 6. 8 代表_cmd从第8位开始; 7. i 表示int类型形参,占用4个字节; 8. f 表示float类型的形参,占用4个字节;
-
在类对象内部也维护了一个方法缓存列表cache_t,当对象第一次调用一个方法的时候,为了提高效率,该方法会被缓存到这个列表中,实际上维护的是一个散列表。
image.png -
那么cache_t是怎样做到方法缓存优化的呢?
其核心就是维护了一张散列表(默认散列表的长度是4),从算法的角度来讲就是以空间换时间。当缓存一个名为test的方法时,先使用test & _mask得出散列表的下标,这个下标的值必定是小于_mask的值,然后在将方法名和函数体指针封装成bucket_t根据下标放到散列表_buckets中。
值得注意的是:方法名 & _mask得到的下标值index会有重复的情况,这时候发现散列表中的下标位置是有值的那么就会index会进行减1操作直到找到空位置存储。(当然也会判断下标位置中的key是否一致,如果一致直接覆盖)
还有存满的情况,也就是上面那种情况,直到下标值到0 还是没有空位置,就从最大的下标继续查找直到begain的位置。当散列表存满的时候,就对散列表扩容至原基础上的两倍大,再清除缓存,重新存储。
-
当一个对象调用一个方法的时候,最终会转成objc_messageSend给class发送消息,如果最后查到superclass为nil,就会进入动态方法解析流程,如果动态解析还没处理就会进入消息转发流程;
- 消息发送流程如下图:
大致主要流程是:
1. 通过对像的isa找到类对象或元类对象;
2. 从类对象中的缓存cache_t查找,这里假装没有;
3. 从类对象中class_rw_t的方法列表中查找,这里又假装没有;(如果查到了,会存到对象的cache_t里面去,流程结束)
4. 从父类中查找,直到superclass为nil,进入动态方法解析流程;(如果父类找到了,回到第3步)
image.png
- 动态方法解析流程:
动态方法解析流程只会进入一次
1. 不管对象有没有实现类方法resolveInstanceMethod或resolveClassMethod,都会尝试调用并再一次进入消息发送流程。这里假装实现了;
2. 在方法内部可以通过class_addMethod为该方法动态添加一个实现,这里假装没有实现;
3. 进入消息转发流程(动态添加实现了 没有这一步了);
image.png
- 消息转发流程:
如果是类方法,以下几种涉及的方法就改成类方法
1. 调用forwardingTargetForSelector:方法,
返回能处理这个方法的对象。这里假装返回nil;
2. 调用methodSignatureForSelector:方法,
返回方法签名(即返回值、参数类型编码)。这里假装返回了;
3. 调用forwardingInovacation:方法,
invocation中包含了方法接收者、方法名、方法签名。执行invoke就会重新发送消息。其实到了这一步就标志着流程结束了;
image.png
- super的理解:使用super调用方法,意思就是从父类开始查找这个方法并调用,
但实际接收者还是self
。super最终会转成objc_messageSendSuper方法发送消息,参数只是比bjc_messageSend多传了一个父类。
-
Runloop
-
Runloop就是保证程序不被退出,并且在运行过程中持续做一些事情。iOS程序中的触摸事件、定时器、GCD、网络等事件都依赖于runloop。runloop能够做到有事情就马上处理,没事情就挂起,不消耗cpu,性能高的特点;
-
Runloop与线程是一一对应的,每一个线程都有一个runloop,但是runloop并不是自动创建的,只有在获取的时候才会去创建runloop。在iOS中主线程的runloop是自动创建的。在程序中有一个全局的字典以线程为key保存着所有的runloop。runloop也会随着线程销毁而消失;
-
runloop可以存在多个运行模式model,但是一次只会运行一个模式,在mode进行切换的时候,只能先退出(这里的退出并不是循环),再重新进入。每个model中包含了多个source0和source1(触摸事件等)、timer(定时器事件)、observer(监听事件),没有这几个都是空的runloop会立即退出;
image.png- source0:处理触摸事件处理和performSelector:onThread:
- source1: 处理基于port的线程间通信 和系统事件的捕捉(屏幕触摸事件先是经过source1包装再交给source0处理的)
- Timers: 处理NSTimer 和performSelector:withObject:afterDelay:
- Observers: 监听runloop的状态,处理UI刷新和AutoreleasePool,都是在runloop即将进入睡眠之前操作的。
-
常用的runloop模式有default和tricking,前者是默认的模式,后者则是scrollview滚动时的model。commonModels则是包含了这两种模式;
-
runloop运行逻辑
image.png
-
-
runloop的休眠实现原理,runloop的休眠能够真正做到cpu不做任何事情。这里主要涉及到状态的切换,需要休眠就会切换到内核态,如果有消息要要处理就从内核态切换到用户态;
-
runloop控制线程保活
# 启动线程永驻
self.thread = [[NSThread alloc] initWithBlock:^{
NSLog(@"begain a thread ---------------");
// 添加一个port任务 不至于让thread因为没有任务导致退出
[[NSRunLoop currentRunLoop] addPort:NSPort.new forMode:NSDefaultRunLoopMode];
//每执行完一个任务 这个循环就会重新执行一次
while (!weakself.isStopThread && weakself != nil) {
//关键点:runloop内部会开一个无限循环 有任务做事 无任务休眠; 默认在每执行一次任务就跳出内部循环
bool result = [NSRunLoop.currentRunLoop runMode:NSDefaultRunLoopMode beforeDate:NSDate.distantFuture];
NSLog(@"%@",result ? @"runloop 运行成功" : @"runloop 运行失败");
}
NSLog(@"end a thread -----------------");
}];
[self.thread start];
#执行任务
if (self.thread) {
//waitUntilDone: 等到任务执行完才往下走
[self performSelector:@selector(stopThread) onThread:self.thread withObject:nil waitUntilDone:true];
}
# 停止线程
// 使用thread执行这个方法
- (void)stopThread {
NSLog(@"%s %@", __func__, NSThread.currentThread);
CFRunLoopStop(CFRunLoopGetCurrent());
_stopThread = true;
self.thread = nil;
}
-
多线程
-
多线程的使用方式有四种:
image.png -
线程的队列执行的方式分为同步和异步两种,同步执行不会重新创建线程,就在当前线程执行。异步执行在新的线程中执行,可能会创建新的线程(在主队列就不会创建新的线程);同步异步主要的区别是会不会创建新的线程;
-
队列的类型分为串行和并发队列,串行队列就是一个个去执行任务,异步队列就会有多个线程同时去执行任务;
image.png -
死锁的情况:如果在串行队列queue_A中的任务S1中使用sync添加同步任务S2并且该任务的执行队列依然是queue_A,就会产生死锁。本质就是因为S1要执行完任务必须得执行完任务S2,但是由于S2是在S1中执行,由于S1跟S2在同一个同步队列中,所以S2要想执行,必须等S1执行完。因此就产生了相互等待,就死锁了;
同步队列queue_A | |
---|---|
S1任务 | S2属于S1中要执行的任务 |
S2任务 | S2也是一个任务,但是它必须排在S1后面执行,同时S1又必须得等S2执行完 |
S3任务 | |
S4任务 | |
...... |
- GCD中group的使用
如果想在一个并发队列中先执行S1和S1任务,等这个任务执行完之后再执行S3。
1. 创建一个group和一个并发队列queue;
2. 将S1和S2分别添加到queue和group中;
3. 在使用group的notify执行S3;
- 线程同步方案:
- OSSpinLock——自旋锁,是一种忙等的锁,相当于写了一个while循环,会一直占用cpu资源。;
- 初始化:OS_SPINKLOCK_INIT
- 尝试加锁:OSSpinLockTry
- 加锁:OSSpinkLockLock
- 解锁:OSSpinkLockUnlock
目前这把锁并不是安全的,它会出现线程优先级反转的问题。因为多线程在并发执行的时候,是cpu每一个线程轮流分配一点时间,只是这个时间分配的非常的短,感觉像是同时执行的。然而在CPU在分配给线程的时间依赖于线程的优先级,如果优先级高CPU分配给该线程执行任务的时间会更长。所以讲线程A和线程B在同时执行同一个任务的时候,如果线程A的优先级高于线程B,在线程B加锁后,线程A此时进来发现被加锁了,会在原地一直等待(会一直占用CPU资源)。这是由于线程A的优先级更高,所以cpu会一直分配资源给线程A执行任务。这个时候线程B由于优先级低导致没有资源科执行任务,也就导致当前的任务执行不完,线程B也无法释放锁。最终可能形成死锁;
- os_unfair_lock - 为了替代OSSpinkLock,在iOS10.0以后出现的一种锁,它解决了OSSpinkLock优先级反转的问题,在碰到加锁的时候,不会去忙等,而是睡眠;
- 初始化:os_unfair_lock_init
- 尝试加锁:os_unfair_lock_try
- 加锁:os_unfair_lock_lock
- 解锁:os_unfair_lock_unlock
- pthread_mutex——从名字来看叫做互斥锁,在线程等待的时候是睡眠处理。互斥锁还有另外一种类型PTHREAD_MUTEX_RECURSIVE是递归锁,它的特性是同一个线程可以重复的加锁开锁,如果不是同一线程就会产生互
斥。
-
引用库:pthread.h
-
初始化:pthread_mutex_init(*, null) 或者PTHREAD_MUTEX_INITIALIZER, 后者是一个结构体宏定义
-
尝试加锁:pthread_mutex_trylock
-
加锁:pthread_mutex_lock
-
解锁:pthread_mutex_unlock
-
条件:pthread_cond_t,可以做到解锁一个线程然后开始睡眠,直到在另外一个线程中通知pthread_cond_t控制睡眠的线程醒来继续加锁并执行。
加锁睡眠:pthread_cond_wait 通知解锁睡眠: pthread_cond_signal(通知单个线程) / pthread_cond_broadcast(通知多个线程)。
最后还需要pthread_cond_destroy销毁; -
销毁:pthread_mutex_destroy
- disptach_semaphore——信号量,用来控制线程最大并发数量
- 创建信号量:dispatch_semephore_create(最大并发数量);
- 开始等待:dispatch_semaphore_wait 就是让信号值减1,如果信号值等于0的时候就会让线程休眠等待,直到信号量的值大于0;
- 信号记录:dispatch_semaphore_signal 让信号值加1,并且通知等待的地方;
- 串行队列同步执行——将多个线程任务放到同一个队列中执行;
- NSLock 是对mutext普通锁的封装
- NSRecursiveLock 是对mutext递归锁的封装
- NSCondition 是对pthread_cond和mutex的封装
- NSConditionLock 是对NSCondition进一步的封装
- synchronized——是对mutex的封装,@synchornized(以某个对象为加锁对象){}。在底层实际上是使用传进去的对象生成另外一个对象,该对象维护的就是一个递归锁;
- 这些锁的性能由高到低排列是:
同步方案的性能表现 |
---|
us_unfair_lock |
OSSPinkLock |
dispatch_semaphore |
pthread_mutex |
dispatch_queue(DISPATCH_QUEUE_SERIAL) |
NSLock |
NSCondition |
pthread_mutext(recursive) |
NSRecursiveLock |
NSConditionlock |
@synchronized |
- 修饰属性关键字atomic
如果使用@property(atomic)定义一个属性A,那么amotic,会在setter和getter方法内部加一个锁,以保证设置属性和获取属性是线程安全的; - 多读单写的解决方案, 多读单写的要求是多个线程可以并发进行读操作,只允许同时一个线程进行写操作,并且在写操作的时候,不能进行读操作;
- 读写锁:pthread_rwlock,读的时候使用pthread_rwlock_rdlock加锁,写的时候使用pthread_rwlock_wrlock加锁;
- 在写操作的时候设立屏障,使用dispatch_barrier_sync(queue),在读操作的时候使用dispatch_async(queue),读写任务必须在同一个队列,而且该队列必须是自己创建的。如果是全局队列或者是串行队列,设立屏障没有效果;
-
内存管理
-
iOS程序的内存分布:内存被一段一段的分隔开来,从低到高可分为:保留区域,代码区域、数据区域、堆区、栈区、内核区;
- 保留区域取决于硬件配置,该区域不能使用;
- 代码区域就是存放编译的代码,所以iOS项目代码大小是有限制的;
- 数据区域存放着字符串常量,已初始化和未初始化的全局变量和静态变量;
- 堆区就是可以有程序员控制的区域,那些通过alloc、malloc、calloc动态分配的空间。分配地址是从低到高;
- 栈区主要是函数中产生的开销,比如局部变量。分配的地址从高到低;
- 内核区系统底层用的区域;
-
Tagged Pointer 标记指针。在64位架构下,对NSNumber、NSString、NSDate小对象进行了优化。将这些类型的数据和类型直接存储到了指针中,不需要动态分配内存,维护引用计数等。当数据在指针中放不下时,才会动态分配内存。如果调用方法,也是调用objc_sendMessage,显然标记指针对象没有isa,所以是直接从指针地址中取出对象的数据,大大的提高了效率。
如果对象指针地址中的二进制的最低或高位(Mac平台是最低位1,iOS平台是最高位1 << 63)是1时,就证明该指针是标记指针
-
OC对象的内存管理
- 根据对象的引用计数器进行内存管理,当对象的计数器为0的时候就释放对象
- xCode支持MRC和ARC两种管理内存模式,MRC就是手动管理内存,需要自己对对象进行retain、release、autorelease操作。而ARC就是自动管理内存,在xcode编译的时候会在对象需要的地方自动添加retain、release和autorelease代码;retain对象引用计数器+1,release对象引用计数器-1,autorelease在适当的时候引用计数器自动-1。当调用方法alloc、new、copy、mutableCopy返回一个对象时,对象的引用计数器就是1;
- OC属性中的关键字,assign == 直接赋值、retain/strong == 在赋值的时候对新值retain对旧值release、copy == 在赋值的时候对旧值release对新值copy;
-
@autoreleasepool自动释放池实现原理:
image.png- autoreleasepool实际是会转成AutoReleasePoolPage对象,每一个autoreleasepool的大小是4096个字节,其中56个字节是存放成员变量的,剩余的就是存放调用了autorelease的对象地址。如果存满就会创建下一个AutoReleasePoolPage对象,并且上一个page对象的child会指向新创建的page,新创建的page就指向上一个page对象。所以它的整体设计是一个双向链表的结构;
- @autoreleasepool是可以嵌套的,每个@autoreleasepool一开始就会调用objc_autoreleasePoolPush,,如果没有page对象就创建一个,并且往栈内放一个POOL_BOUNDARY(其实就是个0),并且返回这个栈顶的地址。每个@autoreleasepool最后都会调用objc_autoreleasePoolPop(),将一开始放进去的POOL_BOUNDARY的地址传进去,表示释放到这个位置,然后就开始释放栈内的地址,直到POOL_BOUNDARY。
查看autoreleasepool内的情况,通过声明C函数extern void _objc_autoreleasePoolPrint()
- autorelease的对象在什么时候释放呢?在主线程的runloop中有两个observer,一个是监听runloop睡眠之前的状态,还有一个是runloop的退出状态,这两个observer都会调用一次objc_autoreleasePoolPop释放对象。在睡眠之前的状态则还会调用一次objc_autoreleasePoolPush;
-
性能优化
- 在屏幕成像的过程中,CPU和GPU起着至关重要的作用。
CPU主要负责对象的创建销毁、对象属性的调整计算、布局的计算、文本的计算和排版、图片的格式转换和解码、图像的绘制等;
GPU主要负责纹理的渲染,在CPU计算完的数据会交给GPU去处理成渲染数据,并且放到一个缓冲区内(在iOS中有两个缓冲区),每收到一次垂直同步信号,就会从缓冲区内取一帧的数据进行渲染; -
屏幕卡顿的原因:
iOS的屏幕渲染是每秒60帧,也就是每过16毫秒就会收到一次垂直同步信号。在每一次收到垂直信号的时候,如果帧缓冲区内没有新的数据,就会显示上一次的数据,这就导致掉帧了;
- 针对CPU的优化:
- 尽量用轻量级的对象,比如不需要处理事件的图层,能用CALayer就不用UIView;
- 不要频繁的调用UIView的相关属性,比如frame、bounds、transform等,尽量减少不必要的修改;
- 尽量提前计算好布局。在需要时一次性调整对象的属性,不要多次修改属性;
- Autolayout布局比frame更耗性能;
- 图片的size最好跟UIImageView的size保持一致,避免重绘带来的性能损耗;
- 控制线程的最大并发数;
- 尽量把耗时的操作放到子线程,比如文本的尺寸计算,绘制,图片的解码和绘制;
- 针对GPU的优化:
- 尽量避免短时间内显示大量的图片,尽可能的多张图片合成一张图片进行展示;
- GPU能处理的最大纹理尺寸是4096*4096,一旦超过这个尺寸,就会占用CPU的资源来处理,所以纹理尽量不要超过这个尺寸;
- 尽量减少视图的数量和层次;
- 减少使用透明的视图,不透明的就设置opaque= true;
- 尽量避免离屏渲染;
-
离屏渲染
- 在openGL中,GPU有两种渲染方式:
1. 当前屏幕渲染:在当前用于显示的屏幕缓冲区进行渲染操作;
2. 离屏渲染:在当前屏幕缓冲区意外另外开辟一个缓冲区进行渲染操作; - 离屏渲染因为需要创建新的缓冲区,并且在屏幕渲染的过程中,需要要多次切换上下文环境,先是从当前屏幕切换到离屏缓冲区,等到离屏结束后,将离屏缓冲区的渲染结果渲染到屏幕上,又需要切回当前屏幕缓冲区,这样的操作比较消耗性能;
- 触发离屏渲染的操作有:
- 光栅化,layer.shouldRasterize = true;
- 遮罩,layer.mask;
- 圆角,同时设置layer.masksToBounds = true, layer.cornerRadius > 0;
- layer.shadowXXX, 如果设置layer.shadowPath就不会产生离屏渲染;
-
耗电优化
- 少用定时器;
- 优化I/O操作,不要频繁写入小数据,最好批量一次性写入。读写大量重要数据时,可以考虑用dispatch_io,其提供了基于GCD的异步操作文件I/O的API,用dispatch_io系统干回优化磁盘访问。数据量比较大的,应该使用数据库;
-
网络优化:
- 减少、压缩网络数据(json、protobuffer);
- 如果多次请求的结果是相同的,尽量使用缓存;
- 使用断点续传,否则网络不稳定时,可能多次传输相同的内容;
- 网络不可用时,不要尝试执行网络请求;
- 让用户可以取消长时间运行或者速度很慢的网络操作,设置合适的超时时间;
- 批量传输:比如下载视频流量时,不要传输很小的数据包,直接下载整个文件或者一大块一大块的下载;
-
定位优化:
- 如果只需要快速定位确定用户的位置,最好用CLLOcationManager的requestLocation方法。因为定位完成后,会自动让定位硬件断电;
- 如果不是导航应用,尽量不要实时更新位置,定位完毕后就关闭定位服务;
- 尽量降低定位精度,比如金莲更不要使用精度最高的KCLLocationAccuracyBest;
- 需要后台定位时,尽量设置pauseLocationUpdatesAutomatically为true,如果用户不太可能移动的时候系统会自动暂停位置更新;
-
App启动优化
- App的启动分为两种,一种是冷启动,就是从零开始启动app,第二种是热启动,App在后台的时候再次启动App。 启动优化主要针对前者;
- 可以通过设置环境变量可以打印出app的启动时间分析,Edit scheme —> Run —> Arguments —> arguments passed on launch,下面添加变量DYLD_PRINT_STATISTICS并设置为1,如果需要更详细的信息DYLD_PRINT_STATISTICS_DETAIL;
- App启动分为三个阶段,先后分为:dyld、runtime、main函数
- Dyld,Apple的动态链接器,可以用来装载Mach-O文件(可执行文件、动态库等);
- Dyld会装载App的可执行文件,同时会递归连接所有依赖的动态库。当dyld把可执行文件、动态库都装载完毕后,会通知runtime进行下一步的处理;
- 优化方案:
- 减少动态库。合并一些动态库,定期清理不必要的动态库;
- 减少objc类和分类的数量、减少selector数量,定期清理不必要的类和分类;
- 减少C++虚函数的数量;
- swift尽量使用struct;
- Runtime会调用map_images进行可执行文件内容的解析和处理,在load_images中调用call_load_methods,调用所有class和category的load方进行各种objc结构的初始化(注册objc类、初始化类对象等等)。还会调用C++静态初始化器和atrribute修饰的函数。 这样可执行文件和动态库中所有的class、protocol、selector、、IMP都已经按格式成功加载到内存中,被runtime所管理;
优化方案:
用+initialize方法配合dispatch_once替代attribute((constructor))、C++静态构造器、Objc的load方法 - Main,App的启动由dyld主导,将可执行文件加载到内存,顺便加载所有依赖的动态库,并有runtime负责加载成objc定义的结构。所有的初始化结束后,dyld就会调用main函数,再调用UIApplicationMain函数,APPDelegate的application:didFinishedLaunchingWithOptions方法。
优化方案:
在不影响用户的前提下,尽量将一些耗时操作延迟,不要全部放到finishLoading方法中;
- Dyld,Apple的动态链接器,可以用来装载Mach-O文件(可执行文件、动态库等);
-
安装包瘦身:
- 安装包主要有可执行文件、资源(图片、音频、视频等)组成;
- 资源可以采用无损压缩;
- 去除没有用到的资源,使用开源项目:https://github.com/tinymind/LSUnusedResources
- 编译器优化(瘦身可执行文件)
- 将工程的Strip lInked Product 、Make Strinfs read-only、Symbols Hidden by Default 设置为true(新的Xcode项目这些值默认就是true),可以去除不必要的调试符号;
- 去掉异常支持,Enable C++ Exception、Enable Objective-Exception设置为false、Other C Flags添加-fno-exceptions(实际验证这一步并没有什么卵用)
- 利用AppCode(IDE)(https://www.jetbrains.com/objc/)检测未使用的代码:菜单栏 -> Code -> Inspect Code
-
LinkMap- 查看每个函数占用的大小
-
架构设计
-
架构属于软件设计方案,具体可到类与类之间的关系、模块与模块之间的关系、客户端与服务端之间的关系。在开发中常用的有MVC、MVP、MVVM,这三种构架都是view和model相互解耦,可独立重复利用。并且都是以controller、presenter、viewModel为媒介处理view层的需要;
-
一般来说架构可以分成:三层架构和四层架构。
三层架构:界面层、业务层、数据层;
四层架构:界面层、业务层、网络层、数据层;
前面的MVC、MVP、MVVM,通产常用于界面层的设计使用;
优秀博客地址:https://github.com/skyming/Trip-to-iOS-Design-Patterns
-
设计模式
image.png
优秀博客地址:https://design-patterns.readthedoc.io/zh_CN/latest
-
数据结构和算法
推荐书籍:
严蔚敏的《数据结构》
《大话数据结构和算法》
-
网络
推荐书籍:
《HTTP权威指南》
《TCP/IP详解卷1:协议》
网友评论