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

你可能感兴趣的:(runtime,runloop,性能优化拾遗)