消息机制:给方法调用者发送消息
1.消息发送
从Class对象一直通过superclass往上找,找遍所有的父类和自己的类
2.动态方法解析
找遍所有的父类和自己的类找不到的话,允许开发者动态去创建一个新的方法,在程序编译阶段没有方法,然后在运行阶段添加方法的实现,叫做动态方法解析
3.消息转发
如果动态方法解析没有做任何的操作,会进入消息转发阶段,会转发给另外一个对象调用方法,自己没有能力去处理一件事,就将消息转发给别人去做
3.1 如果forwardingTargetForSelector:(SEL)aSelector,没有返回要处理的对象,没有告诉要把消息转发给谁的话
3.2 进入methodSignatureForSelector,要求返回一个方法签名(方法签名包括返回值类型,参数类型)
3.3 如果返回的方法签名是一个合理的值,会进入另一个方法forwardInvocation:(NSInvocation *)anInvocation
-(void)forwardInvocation:(NSInvocation *)anInvocation
{
//NSInvocation 封装了一个函数调用,包括方法调用者,方法名,方法参数
//anInvocation.target //方法调用者
//anInvocation.selector //方法名
anInvocation.target =[[Person alloc]init];
[anInvocation invoke];//调用方法
}
3.4 -(void)forwardInvocation:(NSInvocation *)anInvocation,能来到这个方法就可以尽情的处理,想干什么干什么
4.如果以上3个阶段都没有搞定的话就会报一个经典的错误:unrecognized selector sent to instance.
有什么用?
1.如果不想让某一个类调用自己的方法时crash,可以在类里面写以下代码,也可以收集一些信息
要想所有的类都不会因为方法调用crash怎么做呢?那就给NSObject写一个分类
2.使用class_copyIvarList接口,去窥探类的所有成员变量,包括隐藏的成员变量
3.替换系统自带的一些方法实现,hook系统方法的调用做一些事情,hook UIFont设置全局的字体大小,拦截点击事件
4.防止插入数组,字段的对象为nil,导致crash问题
5.解决NSTimer的循环引用问题
runloop与线程的关系?
1.一条线程对应一个runloop,默认情况下线程的runloop是没有创建的,在第一次获取runloop的时候会创建runloop
2.只要往线程中添加runloop就意味着,线程的任务永远不会执行完,就能让线程一直处于激活状态
线程一旦任务执行完,生命周期就结束了,无法再调用了,runloop就是为了让线程一直处于激活状态
timer与runloop的关系?
timer其实就是运行在runloop里面的,相当于是runloop控制timer什么时候执行
runloop是怎么响应用户操作的,具体的流程是什么?
由source1捕捉我们的触摸事件,然后再由source0处理我们的响应事件
runloop的几种状态?
即将进入loop,即将处理Timer,即将处理source,即将进入休眠,刚从休眠中唤醒,即将退出runloop
runloop的mode的作用是什么?
由default模式,有track模式,模式的作用是把不同模式下的source,timer,observer给隔离开来,只会运行一种模式下的source,timer,observer,这样的话在某一种模式下运行时比较流畅的
利用runloop做线程保活的应用场景?
我们需要频繁的在子线程做事情,原来的做法是每次做事情都创建一个新的线程做事情然后销毁,创建一个线程让这个线程一直做这件事情,AFN就用到这种技术,经常需要处理网络请求
线程同步
信号量设置为1,保证线程同步,也可以设置并发数量
解决NSTimer循环引用的更佳方案:NSProxy直接消息转发,不会像继承于NSObject的对象去父类里面搜索,降低效率
GCD的定时器不依赖于runloop,而是和内核挂钩的,会比较准时,定时器的接口设计
+ (NSString *)execTask:(void(^)(void))task
start:(NSTimeInterval)start
interval:(NSTimeInterval)interval
repeats:(BOOL)repeats
async:(BOOL)async;
+ (NSString *)execTask:(id)target
selector:(SEL)selector
start:(NSTimeInterval)start
interval:(NSTimeInterval)interval
repeats:(BOOL)repeats
async:(BOOL)async;
+ (void)cancelTask:(NSString *)name;
定时器的实现
static NSMutableDictionary *timers_;
dispatch_semaphore_t semaphore_;
+ (void)initialize
{
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
timers_ = [NSMutableDictionary dictionary];
semaphore_ = dispatch_semaphore_create(1);
});
}
+ (NSString *)execTask:(void (^)(void))task start:(NSTimeInterval)start interval:(NSTimeInterval)interval repeats:(BOOL)repeats async:(BOOL)async
{
if (!task || start < 0 || (interval <= 0 && repeats)) return nil;
// 队列
dispatch_queue_t queue = async ? dispatch_get_global_queue(0, 0) : dispatch_get_main_queue();
// 创建定时器
dispatch_source_t timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, queue);
// 设置时间
dispatch_source_set_timer(timer,
dispatch_time(DISPATCH_TIME_NOW, start * NSEC_PER_SEC),
interval * NSEC_PER_SEC, 0);
dispatch_semaphore_wait(semaphore_, DISPATCH_TIME_FOREVER);
// 定时器的唯一标识
NSString *name = [NSString stringWithFormat:@"%zd", timers_.count];
// 存放到字典中
timers_[name] = timer;
dispatch_semaphore_signal(semaphore_);
// 设置回调
dispatch_source_set_event_handler(timer, ^{
task();
if (!repeats) { // 不重复的任务
[self cancelTask:name];
}
});
// 启动定时器
dispatch_resume(timer);
return name;
}
+ (NSString *)execTask:(id)target selector:(SEL)selector start:(NSTimeInterval)start interval:(NSTimeInterval)interval repeats:(BOOL)repeats async:(BOOL)async
{
if (!target || !selector) return nil;
return [self execTask:^{
if ([target respondsToSelector:selector]) {
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
[target performSelector:selector];
#pragma clang diagnostic pop
}
} start:start interval:interval repeats:repeats async:async];
}
+ (void)cancelTask:(NSString *)name
{
if (name.length == 0) return;
dispatch_semaphore_wait(semaphore_, DISPATCH_TIME_FOREVER);
dispatch_source_t timer = timers_[name];
if (timer) {
dispatch_source_cancel(timer);
[timers_ removeObjectForKey:name];
}
dispatch_semaphore_signal(semaphore_);
}
tagged Pointer
1.从64bit开始,iOS引入了tagged Pointer技术,用于优化NSNumber,NSDate,NSString等小对象存储
2.判断指针是不是taggedPointer,判断最低有效位是不是1(Mac os),判断最高有效位是不是1(iOS),是的话是taggedPointer
3.taggedPoint少了很多函数的调用,直接从内存地址中取出值,提高了性能,当指针不够存储数据时,才会使用动态分配内存方式来存储数据
4.objc_msgSend能识别tagged Pointer,比如NSNumber的intValue方法,直接从指针提取数据,节省以前的调用开销
拷贝的目的是生成新的对象和原来对象互不影响.
@property (copy,nonatomic) NSMutableArray *data; //这段代码有什么问题
[_data addObject:"111"]; //找不到方法crash
//原因
-(void)setData:(NSMutableArray *)data
{
if(_data!=data)
{
[_data release];
[_data copy]; //返回不可变数组
}
}
一般字符串都设置成copy
@property (copy,nonatomic) NSString *text;
之所以这么做的一个应用场景是:防止对文本框赋值之后,之后在改变字符串时,文本框内容跟着改变就很诡异了
NSMutableString *str =[[NSMutableString alloc]initWithString:@"test"];
UITextField *textField;
textField.text =str;
[str appendString:@"456"];
ARC都帮我们做了什么?
ARC是LLVM和Runtime的协作,ARC利用LLVM编译器,自动帮我们生成了release,retain和autorelease这些代码,在运行时帮我们做一些操作
AutoreleasePoolPage
1.每个AutoreleasePoolPage对象占用4096个字节,除了用来存储它的内部成员变量,剩下的空间用来存储调用autorelease的对象地址值
2.所有的AutoreleasePoolPage对象都是通过双向链表的形式连接在一起的
3.AutoreleasePoolPage用来存放用来调用AutoreleasePool的对象的地址值(一个地址值占8个字节)
1.首先会在AutoreleasePool开始的时候调用Push,把一个POOL_BOUNDRY入栈,返回一个内存地址
autoreleasePoolobj = objc_autoreleasePoolPush();
2.然后在AutoreleasePool结束的时候调用Pop,会把objc_autoreleasePoolPush()的返回值autoreleasePoolobj传入,它会从AutoreleasePoolPage中最后一个入栈的对象地址开始调用release,直到遇到刚才传入的autoreleasePoolobj地址为止
objc_autoreleasePoolPop(autoreleasePoolobj);
next永远存放下一个可以存放autorelease对象的地址
Person *person = [[[Person alloc]init]autorelease]; //
//这个person什么时候调用release是由Runloop来控制的,它可能是在所属的那次runloop循环中,Runloop休眠之前调用了release.
-(void)viewDidLoad
{
[super viewDidLoad];
Person *person = [[[Person alloc]init]autorelease];
}
//如果ARC给person加的是autorelease的话,不会在出了viewDidLoad大括号的时候释放,而是在Runloop休眠之前调用了release.
//如果ARC给person加的是release的话,在出了viewDidLoad大括号的时候就会释放
Runloop和Autorelease
iOS在主线程的Runloop中注册了2个Observer
1.第一个Observer监听了kCFRunLoopEntry事件,会调用 objc_autoreleasePoolPush(),也就意味着runloop执行循环之前会先调用一个objc_autoreleasePoolPush()
2.第二个Observer,监听了kCFRunLoopBeforeWaiting事件,休眠之前会调用objc_autoreleasePoolPop(),objc_autoreleasePoolPush()
CPU(Central Processing Unit,中央处理器)
对象的创建和销毁,对象属性的调整,布局计算,文本的计算和排版,图像的格式转换和解码,图像的绘制
GPU (Graphics Processing Unit,图形处理器)
GPU:纹理的渲染
卡顿产生的原因:
CPU的计算和GPU的渲染,等待垂直信号过来渲染到屏幕上,如果垂直信号过来了,渲染还没有完成就会显示上一帧画面,出现卡顿
解决卡顿的思路:尽可能减少CPU,GPU的消耗
卡顿优化-CPU
1.尽量使用轻量级的对象,比如显示数组能用基本数据类型就不要用NSNumber
2.用不到事件处理的地方,可以考虑用CALayer取代UIView,CALayer是UIView里面的一个成员一个属性,CALayer是用来显示内容的,UIView是用来监听点击事件的
3.尽量不要频繁的调用UIView的一些属性,比如frame,bounds,transform等属性,重新调整肯定要重新计算它布局的一些东西,重新渲染耗费的性能就比较多了,尽量提前计算好布局,需要时一次性调整他们
4.autolayout会比直接设置frame消耗更多的CPU资源,对性能要求高的话能不用就不用
5.图片的size最好刚好跟UIImageView的size保持一致,不一致的话CPU会对图片做一个伸缩的操作
6.控制一下最大的并发数量,不要无限制增加消耗CPU性能
7.尽量把耗时的操作放到子线程, 文本的处理(尺寸的计算,绘制)
//尺寸的计算
[@"text" boundingRectWithSize:CGSizeMake(100,MAXFLOAT) options:NSStringDrawingUsesLineFragmentOrigin attributes:nil context:nil];
//文本的绘制
[@"text" drawWithRect:CGRectMake(100, 100, 100, 100) options:NSStringDrawingUsesLineFragmentOrigin attributes:nil context:nil];
图片的处理(解码,绘制也可以放到子线程做的)
- (void)image
{
UIImageView *imageView = [[UIImageView alloc] init];
imageView.frame = CGRectMake(100, 100, 100, 56);
[self.view addSubview:imageView];
self.imageView = imageView;
dispatch_async(dispatch_get_global_queue(0, 0), ^{
// 获取CGImage
CGImageRef cgImage = [UIImage imageNamed:@"timg"].CGImage;
// alphaInfo
CGImageAlphaInfo alphaInfo = CGImageGetAlphaInfo(cgImage) & kCGBitmapAlphaInfoMask;
BOOL hasAlpha = NO;
if (alphaInfo == kCGImageAlphaPremultipliedLast ||
alphaInfo == kCGImageAlphaPremultipliedFirst ||
alphaInfo == kCGImageAlphaLast ||
alphaInfo == kCGImageAlphaFirst) {
hasAlpha = YES;
}
// bitmapInfo
CGBitmapInfo bitmapInfo = kCGBitmapByteOrder32Host;
bitmapInfo |= hasAlpha ? kCGImageAlphaPremultipliedFirst : kCGImageAlphaNoneSkipFirst;
// size
size_t width = CGImageGetWidth(cgImage);
size_t height = CGImageGetHeight(cgImage);
// context
CGContextRef context = CGBitmapContextCreate(NULL, width, height, 8, 0, CGColorSpaceCreateDeviceRGB(), bitmapInfo);
// draw
CGContextDrawImage(context, CGRectMake(0, 0, width, height), cgImage);
// get CGImage
cgImage = CGBitmapContextCreateImage(context);
// into UIImage
UIImage *newImage = [UIImage imageWithCGImage:cgImage];
// release
CGContextRelease(context);
CGImageRelease(cgImage);
// back to the main thread
dispatch_async(dispatch_get_main_queue(), ^{
self.imageView.image = newImage;
});
});
}
卡顿优化-GPU
1.尽量避免离屏渲染
圆角,同时设置layer.maskToBounds = YES,layer.cornerRadius>0 会导致离屏渲染
考虑用CoreGraphics绘制裁剪圆角,或者直接提供圆角图片
2.尽量避免短时间大量图片显示,尽可能将多张图片合并成一张显示
3.减少透明视图的使用(alpha<1)
4.尽量减少视图的数量和层次
卡顿检测:卡顿主要是主线程执行了比较耗时的操作, 等到垂直信号过来了还没计算完毕导致的卡帧
怎么计算耗时呢?可以添加Observer到主线程Runloop中,通过监听Runloop状态切换的耗时,以达到检测卡顿的目的
实现思路:利用runloop去监听状态,发现两次的时间差比较久就打印堆栈信息
1.创建Observer监听所有状态,添加Observer到Runloop中
2.写一个white循环,在休眠之前做一些什么事情,如果发现5次以上都卡了,打印主线程的堆栈的方法调用信息
耗电优化
1.尽量减少CPU,GPU的功耗
2.少用定时器
3.I/O优化
尽量不要频繁写入小数据,最好批量写入
数据量大的话,考虑用优化过的数据库(SQLite,CoreData)
4.网络优化
较少,压缩网络数据
多次请求同一个URL数据都一样,尽量使用缓存,使用断点续传减少相同数据的传输
网络不可用不要执行网络请求,网络不好时要设置超时,让用户可以取消网络请求(比如点击返回取消请求)
5.定位优化
1.如果只是想确定用户位置,最好用CLLocationManager的requestLocation方法,定位完成后会让定位硬件自动断电
2.不是导航应用,不要实时更新位置,尽量降低定位的精准度
3.使用一些方法,用户不移动时,暂停位置更新
APP的启动
通过添加环境变量打印出APP的启动时间分析(Edit scheme ->Run ->Arguments)
DYLD_PRINT_STATISTICS设置为1
如果需要更详细的信息,那就将DYLD_PRINT_STATISTICS_DETAILS设置为1
1.APP的启动由dyld主导,将可执行文件加载到内存,顺便加载所有依赖的动态库
减少一些动态库,合并一些动态库,定期清理一些不必要的动态库
减少objc中类和分类的数量
较少C++虚函数的数量,一旦有虚函数就会多维护一张表,相对会消耗一些时间
尽量不在+(void)load方法中做事情,尽量在+(void)initialize中做事情
按需加载,尽可能延迟一些操作,不要全部放在finishLaunching方法中加载
2.并由runtime负责加载objc定义的结构(类,分类)
3.所有的初始化工作结束后,dyld就会调用main函数
安装包瘦身
安装包(IPA)主要是由可执行文件(代码编译的文件),资源(图片,视频)组成
资源(图片,音频,视频)
无损压缩
去除没用到的资源,把没有用到的源代码去掉
@dynamic 不要让编译器生成getter ,setter方法的实现,不要生成成员变量