runtime,runloop,性能优化拾遗

消息机制:给方法调用者发送消息

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的做法.png

要想所有的类都不会因为方法调用crash怎么做呢?那就给NSObject写一个分类

2.使用class_copyIvarList接口,去窥探类的所有成员变量,包括隐藏的成员变量

3.替换系统自带的一些方法实现,hook系统方法的调用做一些事情,hook UIFont设置全局的字体大小,拦截点击事件

4.防止插入数组,字段的对象为nil,导致crash问题

5.解决NSTimer的循环引用问题

NSPorxy弱引用Target对象.png
runtime在项目中的应用.png

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,保证线程同步,也可以设置并发数量

信号量设置为1,保证线程同步.png

解决NSTimer循环引用的更佳方案:NSProxy直接消息转发,不会像继承于NSObject的对象去父类里面搜索,降低效率

NSProxy的用法.png

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);
AutoreleasePoolPage结构.png

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.png

卡顿产生的原因:

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方法的实现,不要生成成员变量

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,524评论 5 460
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,869评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,813评论 0 320
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,210评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,085评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,117评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,533评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,219评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,487评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,582评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,362评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,218评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,589评论 3 299
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,899评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,176评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,503评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,707评论 2 335

推荐阅读更多精彩内容

  • runtime 和 runloop 作为一个程序员进阶是必须的,也是非常重要的, 在面试过程中是经常会被问到的, ...
    SOI阅读 21,758评论 3 63
  • 1 Runloop机制原理 深入理解RunLoop http://www.cocoachina.com/ios/2...
    Kevin_Junbaozi阅读 3,954评论 4 30
  • runtime 和 runloop 作为一个程序员进阶是必须的,也是非常重要的, 在面试过程中是经常会被问到的, ...
    made_China阅读 1,198评论 0 7
  • Runloop 是和线程紧密相关的一个基础组件,是很多线程有关功能的幕后功臣。尽管在平常使用中几乎不太会直接用到,...
    jackyshan阅读 9,836评论 10 75
  • 面向对象的三大特性:封装、继承、多态 OC内存管理 _strong 引用计数器来控制对象的生命周期。 _weak...
    运气不够技术凑阅读 1,070评论 0 10