iOS-性能优化总结二(附实例代码)

目录
  • Foundation
    • 缓存 NSDateFormatter 的结果
    • 寻找(NSDate *)dateFromString:(NSString )string 的替换品。
    • sqlite3的正确使用
    • 不要随意使用 NSLog()
    • NSString
    • 获取文件属性
  • UIKit
    • UIImage imageNamed:imageWithContentsOfFile:选择
    • UIView
  • QuartzCore
    • CALayer
  • 如何预防性能问题
    • 内存泄露检测工具
    • FPS性能监测工具
一 Foundation
1.1 缓存 NSDateFormatter 的结果

首先,过度的创建NSDateFormatter用于NSDate与NSString之间转换,会导致App卡顿,打开Profile工具查一下性能,你会发现这种操作占CPU比例是非常高的。据官方说法,创建NSDateFormatter代价是比较高的,如果你使用的非常频繁,那么建议你缓存起来,缓存NSDateFormatter一定能提高效率。

Creating a date formatter is not a cheap operation. If you are likely to use a formatter frequently, it is typically more efficient to cache a single instance than to create and dispose of multiple instances. One approach is to use a static variable.

  • 实例代码

NSDateFormatter

/// use NSDateFormatter
- (void)convertDateToStringUsingDateFormatter {
    old = CFAbsoluteTimeGetCurrent();
    for (int i = 0; i < TIMES; i++) {
        NSDateFormatter *formatter = [[NSDateFormatter alloc] init];
        [formatter setLocale:[[NSLocale alloc] initWithLocaleIdentifier:@"zh_CN"]];
        [formatter setDateFormat:@"yyyy-MM-dd HH:mm:ss"];
        self.dateStr = [formatter stringFromDate:[NSDate date]];
    }
    now = CFAbsoluteTimeGetCurrent();
    NSLog(@"convertDateToStringUsingDateFormatter: %f",now - old);
}

struct tm

/// use struct tm
- (void)convertDateToStringUsingCLocaltime {
    old = CFAbsoluteTimeGetCurrent();
    for (int i = 0; i < TIMES; i++) {
        time_t timeInterval = [NSDate date].timeIntervalSince1970;
        struct tm *cTime = localtime(&timeInterval);
        self.dateStr = [NSString stringWithFormat:@"%d-%02d-%02d %02d:%02d:%02d", cTime->tm_year + 1900, cTime->tm_mon + 1, cTime->tm_mday,cTime->tm_hour, cTime->tm_min, cTime->tm_sec];
    }
    now = CFAbsoluteTimeGetCurrent();
    NSLog(@"convertDateToStringUsingDateFormatter: %f",now - old);
}

运行结果

image.png

可知NSDateFormatter所花费的时间更长。

NSDateFormatter不是唯一一个创建的开销就很昂贵的类,但是它却是常用的、开销大到 Apple 会特别建议应该缓存和重复使用实例的一个。

通用的缓存 NSDateFormatter 的方法

一种通用的缓存 NSDateFormatter 的方法是使用 -[NSThread threadDictionary](因为 NSDateFormatter 不是线程安全的):

@implementation DateTool
+ (NSDateFormatter *)cachedDateFormatter {
    NSMutableDictionary *threadDictionary = [[NSThread currentThread] threadDictionary];
    NSDateFormatter *dateFormatter = [threadDictionary objectForKey:@"cachedDateFormatter"];
    if (dateFormatter == nil) {
        dateFormatter = [[NSDateFormatter alloc] init];
        [dateFormatter setLocale:[NSLocale currentLocale]];
        [dateFormatter setDateFormat: @"YYYY-MM-dd HH:mm:ss"];
        [threadDictionary setObject:dateFormatter forKey:@"cachedDateFormatter"];
    }
    return dateFormatter;
}
@end

// 使用
- (void)getCurTime {
    NSDateFormatter *dateFormatter = [DateTool cachedDateFormatter];
    NSDate* now = [NSDate date];
    NSString* dateString = [dateFormatter stringFromDate:now];
    NSLog(@"%@", dateString);
}
image.png
1.2 寻找 (NSDate *)dateFromString:(NSString )string 的替换品。
- (NSDate *)dateFromString:(NSString *)string;

时间戳宏定义

// 2019-04-06 20:11:13
#define kTimeInterval @"1554552673"
#define kTimeInterval2 @"1554552999"
  • 实例代码 - 使用dateFromString:创建NSDate对象
// timeInterval - 1554552673
- (NSDate *)dateFromString:(NSString *)timeInterval {
    NSDateFormatter *formatter = [[NSDateFormatter alloc] init];
    [formatter setLocale:[[NSLocale alloc] initWithLocaleIdentifier:@"zh_CN"]];
    [formatter setDateFormat:@"yyyy-MM-dd HH:mm:ss"];
    NSDate *date = [formatter dateFromString:timeInterval];
    return date;
}
  • 使用dateWithTimeIntervalSince1970:创建NSDate对象
// timeInterval - 2019-04-06 20:11:13
- (NSDate *)strptimeFromString:(NSString *)timeInterval {
    time_t t;
    struct tm tm;
    strptime([timeInterval cStringUsingEncoding:NSUTF8StringEncoding], "%Y-%m-%dT%H:%M:%S%z", &tm);
    tm.tm_isdst = -1;
    t = mktime(&tm);
    NSDate *date = [NSDate dateWithTimeIntervalSince1970:t + [[NSTimeZone localTimeZone] secondsFromGMT]];
    return date;
}

对比两个时间差,比较1024 * 10次,看看所花费的时间

// dateFromString
- (void)compareTime {
    old = CFAbsoluteTimeGetCurrent();
    for (int i = 0; i < TIMES; i++) {
        NSDate* startDate = [self dateFromString:@"1554552673"];
        NSDate* endDate = [self dateFromString:@"1554552999"];
        NSTimeInterval time = [endDate timeIntervalSinceDate:startDate];
    }
    now = CFAbsoluteTimeGetCurrent();
    NSLog(@"convertDateToStringUsingDateFormatter: %f",now - old);
}

// dateWithTimeIntervalSince1970
- (void)compareTime2 {
    old = CFAbsoluteTimeGetCurrent();
    for (int i = 0; i < TIMES; i++) {
        NSDate* startDate = [self strptimeFromString:@"2019-04-06 20:11:13"];
        NSDate* endDate = [self strptimeFromString:@"2019-04-06 20:46:13"];
        NSTimeInterval time = [endDate timeIntervalSinceDate:startDate];
    }
    now = CFAbsoluteTimeGetCurrent();
    NSLog(@"convertDateToStringUsingDateFormatter: %f",now - old);
}

运行结果

image.png

这可能是最常见的 iOS 性能瓶颈。经过多方努力寻找,上面的方法是转成 NSDateNSDateFormatter 的最著名替代品。

1.2.2 求时间
  • strptime
+ (NSDate *)easyDateFormatter{
    time_t t;
    struct tm tm;
    char *iso8601 = "2016-09-18";
    strptime(iso8601, "%Y-%m-%d", &tm);
    
    tm.tm_isdst = -1;
    tm.tm_hour = 0;//当tm结构体中的tm.tm_hour为负数,会导致mktime(&tm)计算错误
    
    /**
     //NSString *iso8601String = @"2016-09-18T17:30:08+08:00";
     //%Y-%m-%d [iso8601String cStringUsingEncoding:NSUTF8StringEncoding]
     
     {
         tm_sec = 0
         tm_min = 0
         tm_hour = 0
         tm_mday = 18
         tm_mon = 9
         tm_year = 116
         tm_wday = 2
         tm_yday = 291
         tm_isdst = 0
         tm_gmtoff = 28800
         tm_zone = 0x00007fd9b600c31c "CST"
     }
     ISO8601时间格式:2004-05-03T17:30:08+08:00 参考Wikipedia
     */
    
    t = mktime(&tm);
    
    //http://pubs.opengroup.org/onlinepubs/9699919799/functions/mktime.html
    //secondsFromGMT: The current difference in seconds between the receiver and Greenwich Mean Time.
    
    return [NSDate dateWithTimeIntervalSince1970:t + [[NSTimeZone localTimeZone] secondsFromGMT]];
}

调用

- (void)strpTimeTest {
    NSDate *date = [DateTool easyDateFormatter];
    NSDateFormatter *dateFormatter = [DateTool cachedDateFormatter];
    NSString* dateString = [dateFormatter stringFromDate:date];
    NSLog(@"dateString = %@",dateString);
}
image.png
1.3 sqlite3
- (void)sqlite3Test {
    sqlite3 *db = NULL;
    NSString *iso8601String = @"2016-09-18";
    
    sqlite3_open(":memory:", &db);
    sqlite3_stmt *statement = NULL;
    sqlite3_prepare_v2(db, "SELECT strftime('%s', ?);", -1, &statement, NULL);
    sqlite3_bind_text(statement, 1, [iso8601String UTF8String], -1, SQLITE_STATIC);
    sqlite3_step(statement);
    int64_t value = sqlite3_column_int64(statement, 0);
    sqlite3_clear_bindings(statement);
    sqlite3_reset(statement);
}

1.4 不要随意使用 NSLog

NSLog(NSString format, …)

NSLog() 写消息到 Apple 的系统日志。当通过 Xcode 变异运行程序时,被写出的日志会展现在调试终端,同时也会写到设备产品终端日志中。此外,系统会在主线程序列化 NSLog() 的内容。即使是最新的 iOS 设备,NSLog() 输出调试信息所花的时间也是无法忽略的。所以在产品环境中推荐尽可能少的使用 NSLog()。

#ifdef DEBUG
// Only log when attached to the debugger
#define DLog(...) NSLog(__VA_ARGS__)
#else
#define DLog(...) /* */
#endif
// Always log, even in production
#define ALog(...) NSLog(__VA_ARGS__)
1.5 NSString
  • +(instancetype)stringWithFormat:(NSString *)format,, …

创建 NSString 不是特别昂贵,但是当在紧凑循环(比如作为字典的键值)中使用时, +[NSString stringWithFormat:] 的性能可以通过使用类似 asprintf 的 C 函数显著提高。

// 1.6 stringWithFormat提升
- (void)stringWithFormatTest {
    NSString *firstName = @"Daniel";
    NSString *lastName = @"Amitay";
    
    char *buffer;
    asprintf(&buffer, "Full name: %s %s", [firstName UTF8String], [lastName UTF8String]);
    NSString *fullName = [NSString stringWithCString:buffer encoding:NSUTF8StringEncoding];
    free(buffer);
    NSLog(@"fullName = %@",fullName);
}
image.png
1.6 获取文件属性

当试图获取磁盘中一个文件的属性信息时,使用[NSFileManager attributesOfItemAtPath:error:]会浪费大量时间读取可能根本不需要的附加属性。这时可以使用stat代替NSFileManager,直接获取文件属性:

  • 实例代码 - NSFileManager
- (void)getFileAttrByFileManager {
    NSString *path = [[NSBundle mainBundle] pathForResource:@"address" ofType:@"plist"];
    NSError *error;
    NSDictionary *attrDict = [[NSFileManager defaultManager] attributesOfItemAtPath:path error:&error];
    if (error) {
        NSLog(@"error = %@",error.description);
    } else {
        NSLog(@"attrDict = %@",attrDict);
    }
}
  • 实例代码 - stat
#import <sys/stat.h>

- (void)getFileAttrByStat {
    NSString *filePath = [[NSBundle mainBundle] pathForResource:@"address" ofType:@"plist"];
    struct stat statbuf;
    const char *cpath = [filePath fileSystemRepresentation];
    if (cpath && stat(cpath, &statbuf) == 0) {
        NSNumber *fileSize = [NSNumber numberWithUnsignedLongLong:statbuf.st_size];
        NSDate *creationDate = [NSDate dateWithTimeIntervalSince1970:statbuf.st_ctime];
        NSDate *modificationDate = [NSDate dateWithTimeIntervalSince1970:statbuf.st_mtime];
        NSLog(@"fileSize = %ld, creationDate = %@, modificationDate = %@",(long)[fileSize integerValue],[self.formatter stringFromDate:creationDate],[self.formatter stringFromDate:modificationDate]);
    }
}

运行结果如下

image.png
二 UIKit
2.1 imageNamed:imageWithContentsOfFile:选择使用
  • imageNamed:

这种方法会首先在系统缓存中根据指定的名字寻找图片,如果找到了就返回。如果没有在缓存中找到图片,该方法会从指定的文件中加载图片数据,并将其缓存起来,然后再把结果返回,下次再使用该名称图片的时候就省去了从硬盘中加载图片的过程。对于相同名称的图片,系统只会把它Cache到内存一次。

适用于会重复加载的小图片,因为系统会自动缓存加载的图片

  • imageWithContentsOfFile:

只是简单的加载图片,并不会将图片缓存起来,图像会被系统以数据方式加载到程序。

当你不需要重用该图像,或者你需要将图像以数据方式存储到数据库,又或者你要通过网络下载一个很大的图像时,可以使用这种方式。

  • 实例代码
- (void)drawImgView {
    self.imgView1.center = CGPointMake(kScreenWidth * 0.25, kScreenHeight * 0.5);
    self.imgView1.image = [UIImage imageNamed:@"gir2"];
    [self.view addSubview:self.imgView1];
    
    self.imgView2.center = CGPointMake(kScreenWidth * 0.75, kScreenHeight * 0.5);
    NSString *imgUrl = [[NSBundle mainBundle] pathForResource:@"gir1" ofType:@"jpg"];
    self.imgView2.image = [UIImage imageWithContentsOfFile:imgUrl];
    [self.view addSubview:self.imgView2];
}

运行结果

image.png
2.2 UIView
  • clearsContextBeforeDrawing属性
注释:If you set the value of this property to NO, you are responsible 
for ensuring the contents of the view are drawn properly in your 
drawRect: method. If your drawing code is already heavily optimized, 
setting this property is NO can improve performance, especially 
during scrolling when only a portion of the view might need to be redrawn.

将 UIView 的属性 clearsContextBeforeDrawing 设置为 NO 在多数情况下可以提高绘制性能,尤其是在你自己用绘制代码实现了一个定制 view 的时候。

  • frame属性

当设置一个 UIView 的 frame 属性时,应该保证坐标值和像素位置对齐,否则将会触发反锯齿降低性能,也有可能引起图形界面的边界模糊(译者注:尤其是涉及到绘制文字时将会引起文字模糊不清,非 retina 设备特别明显)。

一种简单直接的办法就是使用 CGRectIntegral() 自动将 CGRect 的值四舍五入到整数。对于像素密度大于1的设备,可以将坐标值近似为 1.0f / screen.scale 整数倍。

三 QuartzCore
3.1 CALayer
  • allowsGroupOpacity属性
When true, and the layer's opacity property is less than one, the
layer is allowed to composite itself as a group separate from its
parent. This gives the correct results when the layer contains
multiple opaque components, but may reduce performance.

The default value of the property is read from the boolean
UIViewGroupOpacity property in the main bundle's Info.plist. If no
value is found in the Info.plist the default value is YES for
applications linked against the iOS 7 SDK or later and NO for
applications linked against an earlier SDK. 

在 iOS7 中,这个属性表示 layer 的 sublayer 是否继承父 layer 的透明度,主要用途是当在动画中改变一个 layer 的透明度时(会引起子 view 的透明度显示出来)。但是如果你不需要这种绘制类型,可以关闭这个属性来提高性能。

  • drawsAsynchronously属性
/* When true, the CGContext object passed to the -drawInContext: method
 * may queue the drawing commands submitted to it, such that they will
 * be executed later (i.e. asynchronously to the execution of the
 * -drawInContext: method). This may allow the layer to complete its
 * drawing operations sooner than when executing synchronously. The
 * default value is NO. */

/* Any drawing that you do in your delegate’s drawLayer:inContext: 
method or your view’s drawRect: method normally occurs 
synchronously on your app’s main thread. In some situations, 
though, drawing your content synchronously might not offer the best 
performance. If you notice that your animations are not performing 
well, you might try enabling the drawsAsynchronously property on 
your layer to move those operations to a background thread. If you 
do so, make sure your drawing code is thread safe. */
  • shadowPath属性

如果要操作 CALayer 的阴影属性,推荐设置 layer 的 shadowPath 属性,系统将会缓存阴影减少不必要的重绘。但当改变 layer 的 bounds 时,一定要重设 shadowPath。

- (void)layerTest {
    CALayer *layer = [[CALayer alloc] init];
    layer.shadowOpacity = 0.5f;
    layer.shadowRadius = 10.0f;
    layer.shadowOffset = CGSizeMake(0.0f, 10.0f);
    UIBezierPath *bezierPath = [UIBezierPath bezierPathWithRect:layer.bounds];
    layer.shadowPath = bezierPath.CGPath;
}
  • 注释
 /** Letting Core Animation determine the shape of a shadow can be 
expensive and impact your app’s performance. Rather than letting 
Core Animation determine the shape of the shadow, specify the 
shadow shape explicitly using the shadowPath property of CALayer. 
When you specify a path object for this property, Core Animation 
uses that shape to draw and cache the shadow effect. For layers 
whose shape never changes or rarely changes, this greatly improves 
performance by reducing the amount of rendering done by Core 
Animation. */
  • shouldRasterize属性

如果 layer 只需要绘制依此,那么可以设置 CALayer 的属性 shouldRasterize 为 YES。但是如果该 layer 让然会被移动、缩放或者变形,那么将 shouldRasterize 设置为 YES 会损伤绘制性能,因为系统每次绘制完后会尝试再次重绘。

  • 注释
/** When the value of this property is YES, the layer is rendered as a 
bitmap in its local coordinate space and then composited to the 
destination with any other content. Shadow effects and any filters in 
the filters property are rasterized and included in the bitmap. */
四 如何预防性能问题

如何在开发测试阶段发现问题解决问题,是预防性能问题的关键。为此,可以通过一些工具,用于发现各种性能问题。

4.1 内存泄露检测工具

MLeakFinder 是团队成员zepo在github开源的一款内存泄露检测工具。

MLeakFinder能在开发阶段,把内存泄露问题暴露无遗,减少了很多潜在的性能问题。

关于MLeakFinder原理详解请看我的另一篇文章 iOS-MLeaksFinder详解

4.2 FPS性能监测工具条

FPS监测的原理并不复杂,虽然并不百分百准确,但非常实用,因为可以随时查看FPS低于某个阈值时的堆栈信息,再结合当时的使用场景,开发人员使用起来非常便利,可以很快定位到引起卡顿的场景和原因。

关于FPS监测原理及使用请看我的另一篇文章 iOS-卡顿监测-FPS监测(附详细代码及原理讲解)


本文会持续更新,更多内容敬请期待!


文本参考
微信读书 iOS 性能优化总结
iOS App 性能备忘


  • 如有错误,欢迎指正,多多点赞,打赏更佳,您的支持是我写作的动力。

项目连接地址 - PerformanceOptimizeDemo
项目连接地址 - PerformanceOptimize

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

推荐阅读更多精彩内容