runtime,runloop,性能優(yōu)化拾遺

消息機(jī)制:給方法調(diào)用者發(fā)送消息

1.消息發(fā)送

從Class對象一直通過superclass往上找,找遍所有的父類和自己的類

2.動(dòng)態(tài)方法解析

找遍所有的父類和自己的類找不到的話,允許開發(fā)者動(dòng)態(tài)去創(chuàng)建一個(gè)新的方法,在程序編譯階段沒有方法,然后在運(yùn)行階段添加方法的實(shí)現(xiàn),叫做動(dòng)態(tài)方法解析

3.消息轉(zhuǎn)發(fā)

如果動(dòng)態(tài)方法解析沒有做任何的操作,會(huì)進(jìn)入消息轉(zhuǎn)發(fā)階段,會(huì)轉(zhuǎn)發(fā)給另外一個(gè)對象調(diào)用方法,自己沒有能力去處理一件事,就將消息轉(zhuǎn)發(fā)給別人去做

3.1 如果forwardingTargetForSelector:(SEL)aSelector,沒有返回要處理的對象,沒有告訴要把消息轉(zhuǎn)發(fā)給誰的話
3.2 進(jìn)入methodSignatureForSelector,要求返回一個(gè)方法簽名(方法簽名包括返回值類型,參數(shù)類型)
3.3 如果返回的方法簽名是一個(gè)合理的值,會(huì)進(jìn)入另一個(gè)方法forwardInvocation:(NSInvocation *)anInvocation
-(void)forwardInvocation:(NSInvocation *)anInvocation
{
//NSInvocation 封裝了一個(gè)函數(shù)調(diào)用,包括方法調(diào)用者,方法名,方法參數(shù)
//anInvocation.target //方法調(diào)用者
//anInvocation.selector //方法名

anInvocation.target =[[Person alloc]init];
[anInvocation invoke];//調(diào)用方法
}
3.4 -(void)forwardInvocation:(NSInvocation *)anInvocation,能來到這個(gè)方法就可以盡情的處理,想干什么干什么

4.如果以上3個(gè)階段都沒有搞定的話就會(huì)報(bào)一個(gè)經(jīng)典的錯(cuò)誤:unrecognized selector sent to instance.

有什么用?

1.如果不想讓某一個(gè)類調(diào)用自己的方法時(shí)crash,可以在類里面寫以下代碼,也可以收集一些信息

防止類中調(diào)用方法crash的做法.png

要想所有的類都不會(huì)因?yàn)榉椒ㄕ{(diào)用crash怎么做呢?那就給NSObject寫一個(gè)分類

2.使用class_copyIvarList接口,去窺探類的所有成員變量,包括隱藏的成員變量

3.替換系統(tǒng)自帶的一些方法實(shí)現(xiàn),hook系統(tǒng)方法的調(diào)用做一些事情,hook UIFont設(shè)置全局的字體大小,攔截點(diǎn)擊事件

4.防止插入數(shù)組,字段的對象為nil,導(dǎo)致crash問題

5.解決NSTimer的循環(huán)引用問題

NSPorxy弱引用Target對象.png
runtime在項(xiàng)目中的應(yīng)用.png

runloop與線程的關(guān)系?

1.一條線程對應(yīng)一個(gè)runloop,默認(rèn)情況下線程的runloop是沒有創(chuàng)建的,在第一次獲取runloop的時(shí)候會(huì)創(chuàng)建runloop

2.只要往線程中添加runloop就意味著,線程的任務(wù)永遠(yuǎn)不會(huì)執(zhí)行完,就能讓線程一直處于激活狀態(tài)

線程一旦任務(wù)執(zhí)行完,生命周期就結(jié)束了,無法再調(diào)用了,runloop就是為了讓線程一直處于激活狀態(tài)

timer與runloop的關(guān)系?

timer其實(shí)就是運(yùn)行在runloop里面的,相當(dāng)于是runloop控制timer什么時(shí)候執(zhí)行

runloop是怎么響應(yīng)用戶操作的,具體的流程是什么?

由source1捕捉我們的觸摸事件,然后再由source0處理我們的響應(yīng)事件

runloop的幾種狀態(tài)?

即將進(jìn)入loop,即將處理Timer,即將處理source,即將進(jìn)入休眠,剛從休眠中喚醒,即將退出runloop

runloop的mode的作用是什么?

由default模式,有track模式,模式的作用是把不同模式下的source,timer,observer給隔離開來,只會(huì)運(yùn)行一種模式下的source,timer,observer,這樣的話在某一種模式下運(yùn)行時(shí)比較流暢的

利用runloop做線程保活的應(yīng)用場景?

我們需要頻繁的在子線程做事情,原來的做法是每次做事情都創(chuàng)建一個(gè)新的線程做事情然后銷毀,創(chuàng)建一個(gè)線程讓這個(gè)線程一直做這件事情,AFN就用到這種技術(shù),經(jīng)常需要處理網(wǎng)絡(luò)請求

線程同步

信號(hào)量設(shè)置為1,保證線程同步,也可以設(shè)置并發(fā)數(shù)量

信號(hào)量設(shè)置為1,保證線程同步.png

解決NSTimer循環(huán)引用的更佳方案:NSProxy直接消息轉(zhuǎn)發(fā),不會(huì)像繼承于NSObject的對象去父類里面搜索,降低效率

NSProxy的用法.png

GCD的定時(shí)器不依賴于runloop,而是和內(nèi)核掛鉤的,會(huì)比較準(zhǔn)時(shí),定時(shí)器的接口設(shè)計(jì)

+ (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;

定時(shí)器的實(shí)現(xiàn)

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;
    
    // 隊(duì)列
    dispatch_queue_t queue = async ? dispatch_get_global_queue(0, 0) : dispatch_get_main_queue();
    
    // 創(chuàng)建定時(shí)器
    dispatch_source_t timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, queue);
    
    // 設(shè)置時(shí)間
    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);
    // 定時(shí)器的唯一標(biāo)識(shí)
    NSString *name = [NSString stringWithFormat:@"%zd", timers_.count];
    // 存放到字典中
    timers_[name] = timer;
    dispatch_semaphore_signal(semaphore_);
    
    // 設(shè)置回調(diào)
    dispatch_source_set_event_handler(timer, ^{
        task();
        
        if (!repeats) { // 不重復(fù)的任務(wù)
            [self cancelTask:name];
        }
    });
    
    // 啟動(dòng)定時(shí)器
    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技術(shù),用于優(yōu)化NSNumber,NSDate,NSString等小對象存儲(chǔ)

2.判斷指針是不是taggedPointer,判斷最低有效位是不是1(Mac os),判斷最高有效位是不是1(iOS),是的話是taggedPointer

3.taggedPoint少了很多函數(shù)的調(diào)用,直接從內(nèi)存地址中取出值,提高了性能,當(dāng)指針不夠存儲(chǔ)數(shù)據(jù)時(shí),才會(huì)使用動(dòng)態(tài)分配內(nèi)存方式來存儲(chǔ)數(shù)據(jù)

4.objc_msgSend能識(shí)別tagged Pointer,比如NSNumber的intValue方法,直接從指針提取數(shù)據(jù),節(jié)省以前的調(diào)用開銷

拷貝的目的是生成新的對象和原來對象互不影響.

@property (copy,nonatomic) NSMutableArray *data;  //這段代碼有什么問題
[_data addObject:"111"]; //找不到方法crash
//原因
-(void)setData:(NSMutableArray *)data
{
    if(_data!=data)
    {
        [_data release];
        [_data copy];  //返回不可變數(shù)組
    }
}

一般字符串都設(shè)置成copy
@property (copy,nonatomic) NSString *text;
之所以這么做的一個(gè)應(yīng)用場景是:防止對文本框賦值之后,之后在改變字符串時(shí),文本框內(nèi)容跟著改變就很詭異了

  NSMutableString *str =[[NSMutableString alloc]initWithString:@"test"];
  UITextField *textField;
  textField.text =str;
  [str appendString:@"456"];

ARC都幫我們做了什么?

ARC是LLVM和Runtime的協(xié)作,ARC利用LLVM編譯器,自動(dòng)幫我們生成了release,retain和autorelease這些代碼,在運(yùn)行時(shí)幫我們做一些操作

AutoreleasePoolPage

1.每個(gè)AutoreleasePoolPage對象占用4096個(gè)字節(jié),除了用來存儲(chǔ)它的內(nèi)部成員變量,剩下的空間用來存儲(chǔ)調(diào)用autorelease的對象地址值

2.所有的AutoreleasePoolPage對象都是通過雙向鏈表的形式連接在一起的

3.AutoreleasePoolPage用來存放用來調(diào)用AutoreleasePool的對象的地址值(一個(gè)地址值占8個(gè)字節(jié))

1.首先會(huì)在AutoreleasePool開始的時(shí)候調(diào)用Push,把一個(gè)POOL_BOUNDRY入棧,返回一個(gè)內(nèi)存地址

autoreleasePoolobj = objc_autoreleasePoolPush();

2.然后在AutoreleasePool結(jié)束的時(shí)候調(diào)用Pop,會(huì)把objc_autoreleasePoolPush()的返回值autoreleasePoolobj傳入,它會(huì)從AutoreleasePoolPage中最后一個(gè)入棧的對象地址開始調(diào)用release,直到遇到剛才傳入的autoreleasePoolobj地址為止

objc_autoreleasePoolPop(autoreleasePoolobj);
AutoreleasePoolPage結(jié)構(gòu).png

next永遠(yuǎn)存放下一個(gè)可以存放autorelease對象的地址

Person *person = [[[Person alloc]init]autorelease]; //
//這個(gè)person什么時(shí)候調(diào)用release是由Runloop來控制的,它可能是在所屬的那次runloop循環(huán)中,Runloop休眠之前調(diào)用了release.

-(void)viewDidLoad
{
   [super viewDidLoad];
Person *person = [[[Person alloc]init]autorelease];
}

//如果ARC給person加的是autorelease的話,不會(huì)在出了viewDidLoad大括號(hào)的時(shí)候釋放,而是在Runloop休眠之前調(diào)用了release.

//如果ARC給person加的是release的話,在出了viewDidLoad大括號(hào)的時(shí)候就會(huì)釋放

Runloop和Autorelease

iOS在主線程的Runloop中注冊了2個(gè)Observer

1.第一個(gè)Observer監(jiān)聽了kCFRunLoopEntry事件,會(huì)調(diào)用 objc_autoreleasePoolPush(),也就意味著runloop執(zhí)行循環(huán)之前會(huì)先調(diào)用一個(gè)objc_autoreleasePoolPush()
2.第二個(gè)Observer,監(jiān)聽了kCFRunLoopBeforeWaiting事件,休眠之前會(huì)調(diào)用objc_autoreleasePoolPop(),objc_autoreleasePoolPush()

CPU(Central Processing Unit,中央處理器)

對象的創(chuàng)建和銷毀,對象屬性的調(diào)整,布局計(jì)算,文本的計(jì)算和排版,圖像的格式轉(zhuǎn)換和解碼,圖像的繪制

GPU (Graphics Processing Unit,圖形處理器)

GPU:紋理的渲染

CPU和GPU.png

卡頓產(chǎn)生的原因:

CPU的計(jì)算和GPU的渲染,等待垂直信號(hào)過來渲染到屏幕上,如果垂直信號(hào)過來了,渲染還沒有完成就會(huì)顯示上一幀畫面,出現(xiàn)卡頓

解決卡頓的思路:盡可能減少CPU,GPU的消耗

卡頓優(yōu)化-CPU

1.盡量使用輕量級(jí)的對象,比如顯示數(shù)組能用基本數(shù)據(jù)類型就不要用NSNumber

2.用不到事件處理的地方,可以考慮用CALayer取代UIView,CALayer是UIView里面的一個(gè)成員一個(gè)屬性,CALayer是用來顯示內(nèi)容的,UIView是用來監(jiān)聽點(diǎn)擊事件的

3.盡量不要頻繁的調(diào)用UIView的一些屬性,比如frame,bounds,transform等屬性,重新調(diào)整肯定要重新計(jì)算它布局的一些東西,重新渲染耗費(fèi)的性能就比較多了,盡量提前計(jì)算好布局,需要時(shí)一次性調(diào)整他們

4.autolayout會(huì)比直接設(shè)置frame消耗更多的CPU資源,對性能要求高的話能不用就不用

5.圖片的size最好剛好跟UIImageView的size保持一致,不一致的話CPU會(huì)對圖片做一個(gè)伸縮的操作

6.控制一下最大的并發(fā)數(shù)量,不要無限制增加消耗CPU性能

7.盡量把耗時(shí)的操作放到子線程, 文本的處理(尺寸的計(jì)算,繪制)

//尺寸的計(jì)算
    
    [@"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;
        });
    });
}

卡頓優(yōu)化-GPU

1.盡量避免離屏渲染

圓角,同時(shí)設(shè)置layer.maskToBounds = YES,layer.cornerRadius>0 會(huì)導(dǎo)致離屏渲染

考慮用CoreGraphics繪制裁剪圓角,或者直接提供圓角圖片

2.盡量避免短時(shí)間大量圖片顯示,盡可能將多張圖片合并成一張顯示

3.減少透明視圖的使用(alpha<1)

4.盡量減少視圖的數(shù)量和層次

卡頓檢測:卡頓主要是主線程執(zhí)行了比較耗時(shí)的操作, 等到垂直信號(hào)過來了還沒計(jì)算完畢導(dǎo)致的卡幀

怎么計(jì)算耗時(shí)呢?可以添加Observer到主線程Runloop中,通過監(jiān)聽Runloop狀態(tài)切換的耗時(shí),以達(dá)到檢測卡頓的目的

實(shí)現(xiàn)思路:利用runloop去監(jiān)聽狀態(tài),發(fā)現(xiàn)兩次的時(shí)間差比較久就打印堆棧信息

1.創(chuàng)建Observer監(jiān)聽所有狀態(tài),添加Observer到Runloop中
2.寫一個(gè)white循環(huán),在休眠之前做一些什么事情,如果發(fā)現(xiàn)5次以上都卡了,打印主線程的堆棧的方法調(diào)用信息

耗電優(yōu)化

1.盡量減少CPU,GPU的功耗

2.少用定時(shí)器

3.I/O優(yōu)化

盡量不要頻繁寫入小數(shù)據(jù),最好批量寫入

數(shù)據(jù)量大的話,考慮用優(yōu)化過的數(shù)據(jù)庫(SQLite,CoreData)

4.網(wǎng)絡(luò)優(yōu)化

較少,壓縮網(wǎng)絡(luò)數(shù)據(jù)

多次請求同一個(gè)URL數(shù)據(jù)都一樣,盡量使用緩存,使用斷點(diǎn)續(xù)傳減少相同數(shù)據(jù)的傳輸

網(wǎng)絡(luò)不可用不要執(zhí)行網(wǎng)絡(luò)請求,網(wǎng)絡(luò)不好時(shí)要設(shè)置超時(shí),讓用戶可以取消網(wǎng)絡(luò)請求(比如點(diǎn)擊返回取消請求)

5.定位優(yōu)化

1.如果只是想確定用戶位置,最好用CLLocationManager的requestLocation方法,定位完成后會(huì)讓定位硬件自動(dòng)斷電

2.不是導(dǎo)航應(yīng)用,不要實(shí)時(shí)更新位置,盡量降低定位的精準(zhǔn)度

3.使用一些方法,用戶不移動(dòng)時(shí),暫停位置更新

APP的啟動(dòng)

通過添加環(huán)境變量打印出APP的啟動(dòng)時(shí)間分析(Edit scheme ->Run ->Arguments)

DYLD_PRINT_STATISTICS設(shè)置為1

如果需要更詳細(xì)的信息,那就將DYLD_PRINT_STATISTICS_DETAILS設(shè)置為1

1.APP的啟動(dòng)由dyld主導(dǎo),將可執(zhí)行文件加載到內(nèi)存,順便加載所有依賴的動(dòng)態(tài)庫

減少一些動(dòng)態(tài)庫,合并一些動(dòng)態(tài)庫,定期清理一些不必要的動(dòng)態(tài)庫

減少objc中類和分類的數(shù)量

較少C++虛函數(shù)的數(shù)量,一旦有虛函數(shù)就會(huì)多維護(hù)一張表,相對會(huì)消耗一些時(shí)間

盡量不在+(void)load方法中做事情,盡量在+(void)initialize中做事情

按需加載,盡可能延遲一些操作,不要全部放在finishLaunching方法中加載

2.并由runtime負(fù)責(zé)加載objc定義的結(jié)構(gòu)(類,分類)

3.所有的初始化工作結(jié)束后,dyld就會(huì)調(diào)用main函數(shù)

安裝包瘦身

安裝包(IPA)主要是由可執(zhí)行文件(代碼編譯的文件),資源(圖片,視頻)組成

資源(圖片,音頻,視頻)

無損壓縮

去除沒用到的資源,把沒有用到的源代碼去掉

@dynamic 不要讓編譯器生成getter ,setter方法的實(shí)現(xiàn),不要生成成員變量

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • runtime 和 runloop 作為一個(gè)程序員進(jìn)階是必須的,也是非常重要的, 在面試過程中是經(jīng)常會(huì)被問到的, ...
    SOI閱讀 22,029評(píng)論 3 63
  • 1 Runloop機(jī)制原理 深入理解RunLoop http://www.cocoachina.com/ios/2...
    Kevin_Junbaozi閱讀 4,251評(píng)論 4 30
  • runtime 和 runloop 作為一個(gè)程序員進(jìn)階是必須的,也是非常重要的, 在面試過程中是經(jīng)常會(huì)被問到的, ...
    made_China閱讀 1,276評(píng)論 0 7
  • Runloop 是和線程緊密相關(guān)的一個(gè)基礎(chǔ)組件,是很多線程有關(guān)功能的幕后功臣。盡管在平常使用中幾乎不太會(huì)直接用到,...
    jackyshan閱讀 10,018評(píng)論 10 75
  • 面向?qū)ο蟮娜筇匦裕悍庋b、繼承、多態(tài) OC內(nèi)存管理 _strong 引用計(jì)數(shù)器來控制對象的生命周期。 _weak...
    運(yùn)氣不夠技術(shù)湊閱讀 1,231評(píng)論 0 10

友情鏈接更多精彩內(nèi)容