标签:dispatch 缓存 cin uiimage 图片缩放 dev form 快速 图形
逻辑优化
界面优化
代码封装优化
代码的封装优化主要是细化代码的功能,每个功能单独提取出来做成一个方法,当其他地方需要用到同样功能时直接调用该方法即可,无需写重复代码,减少代码量,增加代码的重用性,方便单元测试。
例如:一个过滤输入文本内容的方法,需要过滤特殊字符和表情
- (void)filterCharactorString:(NSString *)string
{
/*过滤表情*/
NSString *modifiedString;
NSRegularExpression *regex = [NSRegularExpression regularExpressionWithPattern:@"[^\\u0020-\\u007E\\u00A0-\\u00BE\\u2E80-\\uA4CF\\uF900-\\uFAFF\\uFE30-\\uFE4F\\uFF00-\\uFFEF\\u0080-\\u009F\\u2000-\\u201f\r\n]"options:NSRegularExpressionCaseInsensitive error:nil];
modifiedString = [regex stringByReplacingMatchesInString:string
options:0
range:NSMakeRange(0, [text length])
withTemplate:@""];
/*过滤特殊字符*/
NSCharacterSet *set = [NSCharacterSet characterSetWithCharactersInString:@"@/:;()¥「」"、[]{}#%-*+=_\\|~<>$€^?‘@#$%^&*()_+‘\""];
int i = 0;
while (i < modifiedString.length) {
NSString *rangeString = [modifiedString substringWithRange:NSMakeRange(i, 1)];
NSRange range = [rangeString rangeOfCharacterFromSet:set];
if (range.length == 0) {
modifiedString = [modifiedString stringByReplacingCharactersInRange:NSMakeRange(i, 1) withString:@""];
}
i++;
}
return modifiedString;
}
上面的方法虽然实现了需要的功能,但是却显不灵活。假如我只想过滤表情,只想过滤特殊字符或者想过滤其他的内容,则需要重新写一个方法来满足功能。但是功能内部的代码却大致相同。这样就增加了代码量且使得代码看起来非常臃肿。
对上面的代码进行封装优化的方案有很多种,见仁见智,主要在思路而不在方法。
例如:我选择把过滤表情单独的提取出来,成一个根据正则表达式来过滤内容的方法,而过滤特殊字符串提取出来,成一个根据传入的字符来过滤内容的方法。
/**
根据正则表达式过滤文字
@param string 需要校验的文字
@param regexStr 用以校验的正则表达式
@return 过滤后的文字
*/
+ (NSString *)filterCharactor:(NSString *)string withRegex:(NSString *)regexStr
{
NSString *searchText = string;
NSError *error = NULL;
NSRegularExpression *regex = [NSRegularExpression regularExpressionWithPattern:regexStr options:NSRegularExpressionCaseInsensitive error:&error];
NSString *result = [regex stringByReplacingMatchesInString:searchText options:NSMatchingReportCompletion range:NSMakeRange(0, searchText.length) withTemplate:@""];
return result;
}
/**
根据传入的字符过滤文本内容
@param string 需要过滤的原文本
@param regexStr 需要过滤的字符内容
@return 过滤后的文字
*/
+ (NSString *)filterSymbol:(NSString *)string withRegex:(NSString *)regexStr
{
NSCharacterSet *set = [NSCharacterSet characterSetWithCharactersInString:regexStr];
int i = 0;
while (i < string.length) {
NSString *rangeString = [string substringWithRange:NSMakeRange(i, 1)];
NSRange range = [rangeString rangeOfCharacterFromSet:set];
if (range.length == 0) {
string = [string stringByReplacingCharactersInRange:NSMakeRange(i, 1) withString:@""];
}
i++;
}
return string;
}
这样是方法中的功能性单一,但针对性却不单一。大大的提高了代码的重用性和单元测试。
代码的封装很重要,体现程序员的编程思维的远见性,代码的可扩展性。在合作开发时,能方便他人
代码执行效率优化
执行效率的优化主要在于得到结果的快慢,例如你想要一样东西,某宝和某东都有且价格差不多,但某宝要两天才能拿到,而某东当天下午就可以拿到。当然大家都会某东啦...这就是效率的优势。
关于代码的执行效率其实还有很多地方,碍于本人目前的眼界和水平有限,后期会验证后添加更多
NSMutableDictionary *dic = [NSMutableDictionary new];
for (int i = 0 ; i < 100; i++) {
[dic setObject:[NSString stringWithFormat:@"%i",i] forKey:[NSString stringWithFormat:@"%i",i]];
}
CFAbsoluteTime forStarTime = CFAbsoluteTimeGetCurrent();
NSArray *dicValueArray = dic.allValues;
for (int i = 0; i < dicValueArray.count; i++) {
NSString *value = dicValueArray[i];
NSLog(@"for----value:%@",value);
}
CFAbsoluteTime forEndTime = CFAbsoluteTimeGetCurrent() - forStarTime;
CFAbsoluteTime forInStarTime = CFAbsoluteTimeGetCurrent();
for (NSString *value in dic.allValues) {
NSLog(@"forIn----value:%@",value);
}
CFAbsoluteTime forInEndTime = CFAbsoluteTimeGetCurrent() - forInStarTime;
CFAbsoluteTime enumerateInStarTime = CFAbsoluteTimeGetCurrent();
[dic enumerateKeysAndObjectsUsingBlock:^(id _Nonnull key, id _Nonnull obj, BOOL * _Nonnull stop) {
NSLog(@"en----value:%@",obj);
}];
CFAbsoluteTime enumerateEndTime = CFAbsoluteTimeGetCurrent() - enumerateInStarTime;
NSLog(@"for循环用时:%f",forEndTime);
NSLog(@"forIn循环用时:%f",forInEndTime);
NSLog(@"enumerateKeysAndObjectsUsingBlock用时:%f",enumerateEndTime);
执行的结果:
for循环用时:0.018385
forIn循环用时:0.017044
enumerateKeysAndObjectsUsingBlock用时:0.016417
看上去是enumerateKeysAndObjectsUsingBlock更快,但是执行多次后 ,你会发现,有时for快有时forin快有时enumerateKeysAndObjectsUsingBlock快。那是因为数据量比较少。
假如把上面代码里有100000个数据。
结果:
for循环用时:20.812115
forIn循环用时:21.940614
enumerateKeysAndObjectsUsingBlock用时:23.253821
for循环明显更快,不论尝试多少次结果都是for循环明显快。之所以用时20多秒是因为循环内部打印了日志,因为打印日志是非常耗时的操作。当然可以不用在内部去打印日志。结果依然是for循环更快。而往往在开发中,for循环内部执行的操作都是比较多并且耗时的。
所以在数据量小时for,forIn,enumerateKeysAndObjectsUsingBlock都可以。在大量数据时,尽量用for循环去执行。经过测试,执行效率上NSDictionary < NSArray < NSSet 。NSSet的执行效率最高
由此可见很多时候在获取特定的数据时算法的选择会即决定了代码执行次数也决定了执行效率。作为一个开发者要了解最基本的各类算法
冒泡排序、快速排序、插入排序、归并排序、希尔排序、动态排序。这些都是提供执行效率的基本算法。必须掌握了解的,这里就不详说了。不懂的朋友度娘
离屏渲染
说到离屏渲染,需要先了解屏幕每一帧界面是如何得到的。
目前IOS设备采用的是双缓存+垂直同步,而Android在4.1后采用的是三缓存+垂直同步。
双缓存机制:GPU会预先渲染好一帧放入下一个缓存区内,让视频控制器取出,当下一帧渲染好后,GPU会直接把视频控制器的指针指向第二个缓冲器,如此来提高效率。即屏幕显示一帧,GPU预备下一帧。而不是显示完一帧在计算下一帧。
首先
,系统图形服务会通过CADisplayLink等机制通知App,然后
,App主线程开始在CPU中计算显示内容,比如视图等创建、布局、图片解码、文本绘制等,接着
,CPU计算好的内容提交到GPU区,由GPU进行变换、合成、渲染。随后GPU将渲染结果提交到帧缓冲区,等到下一次收到VSync信号时显示上一帧计算好的内容。
由上面知识点,可以看出离屏渲染是在GPU中造成的。
屏幕外
缓冲区计算这个位图。当计算好后在转换到帧缓冲区。这一次的渲染是脱离了屏幕而在屏幕以外的区域渲染完成的,所以叫做离屏渲染。创建额外的屏幕外
缓冲区去计算位图,再去替换屏幕内容的代价是非常大且耗时的。解决离屏渲染是提升用户体验非常重要的点,因为离屏渲染会导致帧丢失界面卡顿资源消耗。
补充知识点:
UIView和CALayer的关系
The Relationship Between Layers and Views这里有一篇关于它俩关系的详细说明
简单的说UIView是基于CALayer进行封装的。UIView的每个属性都对应这CALayer的一个属性。
而CALayer负责显示UIView的具体内容,UIView负责提供内容和处理响应事件等,也就是说我们在手机上看见的都是CALayer所呈现的内容。下面是CALayer的结构图
background
背景 、contents
内容、border
边框。而中间的contents的属性声明为var contents: AnyObject?
实际上它必须是个CGImage才能显示。
造成离屏渲染的点:
当设置shouldRasterize = YES时,会把光栅化的图片保存成一个bitmap缓存起来,当下一次要显示这个图层时,CPU会直接从缓存中拿取位图,传给GPU,而不需要GPU再去渲染这一部分的图层,减少GPU的渲染计算。 可以通过Instruments core animation或者模拟器 中的 Color Hits Green and Misses Red来查看图层是否被缓存了,绿色表示缓存,红色表示没有缓存。一般视图shouldRasterize默认为NO,对于经常变换的视图不要使用shouldRasterize。会造成性能消耗的浪费。
关于shouldRasterize是一个有取有舍的属性,对于那些复杂但是内容不长变的视图可以用shouldRasterize来缓存内容,减少GPU每次的计算,达到性能提高。但是要慎用,目前本人项目中还没有使用过shouldRasterize来缓存内容。
屏幕上的每一个像素点是由当前像素点上多层layer通过GPU混合颜色计算出来的,视图的layer一般在最下层,阴影则在视图layer之下。mask是layer的一个属性,它也是CALayer类型的,从官方对该属性的注释可知,默认情况下mask是为nil不存在的。mask相当于一个遮罩层,覆盖在视图的layer的上层,如果视图的layer是contentLayer,那么为这个layer添加一个mask,可以用mask来控制视图显示的区域和透明度。在mask的区域内的contentLayer会被显示,而之外的将不被显示,而区域内的contentLayer将通过mask层把像素颜色传递出去,如果mask的opacity不为1,那么mask将会按照opacity值过滤contentLayer的内容。当为视图设置了mask后,mask的复杂度会决定GPU的计算复杂度,当mask的opacity不为1时或者视图的alpha不为1,那么GPU将进行多层layer的混合颜色计算。
阴影是直接合成一个在视图下面的layer,而不是在下面创建一个新的视图来当做阴影,当阴影的透明度不为1时,它的渲染复杂度会比较大。
allowsEdgeAntialiasing是ios7以后提供的方法,用来抗锯齿,有时候图片缩放或者界面旋转会造成边框出现锯齿。而锯齿的计算是非常耗性能的会造成离屏渲染的。所以在出现锯齿情况下allowsEdgeAntialiasing设置为YES
allowsGroupOpacity是设置视图子视图在透明度上是否跟父视图一样,一般默认情况下是为YES的。如果父视图的透明度不为1,那么子视图的透明度也不会为1。在GPU渲染的时候,就会造成既要渲染子视图还要渲染子视图下面的父视图内容,然后合成视图。这样造成GPU计算复杂度增大需要离屏渲染解决。
这里复杂形分为两种
一种是有系统设置造成的形状,比如设置圆角用maskToBundle加cornerRadius这种是有系统剪裁形成的圆角形状。
另一种是绘制生成的形状,比如图片中有圆角区域外是透明的或者直接绘制圆角。
系统形状会造成GPU的消耗,因为剪裁会很耗性能,而绘制会造成CPU性能消耗高,因为绘制工作是由CPU造成的
渐变的渲染计算是非常负责好性能的。
从上面的点相信你已经了解到了造成离屏渲染的原因。
下面是关于离屏渲染、界面优化的方法
/* 思路:用不透明的mask来实现仅显示圆角内区域。 之所以不采用异步绘制的方式是因为绘制会消耗CPU性能,而且绘制需要考虑是否缓存,如果不缓存每次都需要绘制很耗电,但基于负载平衡的原理,有时也可以采用绘制来减少GPU压力 */
button.frame = CGRectMake(0, 0, 100, 100);
button.backgroundColor = [UIColor redColor];
//显示路径,根据UIRectCorner枚举来控制那些区域需要圆角
UIBezierPath *path = [UIBezierPath bezierPathWithRoundedRect:CGRectMake(0, 0, 100*scale, 100*scale) byRoundingCorners:UIRectCornerAllCorners cornerRadii:CGSizeMake(5, 5)];
CAShapeLayer *mask = [[CAShapeLayer alloc] init];
mask.path = path.CGPath;
mask.frame = CGRectMake(0, 0, 100, 100);
button.layer.mask = mask2;
本人在经过各种测试和观看各种文章资料后百思不得其解,从原理上来说上面指出的离屏渲染的几个点确实会造成离屏渲染。但是我代码测试并查看Color Off-Screen Rendered。居然没有高亮黄色。。。纳尼!!!难道是苹果又做了优化了。。。正在查找苹果文档
接下来说说Color Blender Layer,在模拟器中旋转Debug--> Color Blender Layer。模拟器界面中出现绿色的部分表示没有透明内容,红色的表示有透明内容。对于好的程序来说,绿色越多越好,上面离屏渲染讲过了,透明会造成GPU的计算复杂度变大,需要混合颜色计算。下面来说说解决这个问题的方法
/* 普通的label只需要根据界面需求设置个背景颜色设置maskToBundle为YES,而button中的label把背景颜色设置成跟按钮一个颜色设置maskToBundle为YES, */
label.background = [UIColor redColor];
label.maskToBundle = YES;
(3.)对于图片中有透明区域,这就需要根据界面与设计同学进行调整。虽然现在的处理器越来越强,这些优化微不足道,但对于一个合格的程序员而言,尽善尽美才是追求
(4.)异步加载绘制
知识点:对象的创建,属性的调整等都比较消耗CPU,所以尽量的使用轻量级的对象可以减少CPU的消耗,而CALayer的量级比UIVIew轻许多。所以数据或对象的创建尽量放在异步线程中执行,保证主线程的畅通快速。但包含CALayer的控件都必须在主线程中创建操作,而界面控件一般都是在viewDidLoad里创建的,而系统方法都是在主线程中执行的,具体原因这里可以要说说Runloop的原理,过段时间写一篇关于Runloop原理的文章说明吧。
/*如果viewDidLoad内部代码执行耗时耗性会造成界面跳转显示卡顿,所以我采用异步主队列方式让控件的创建设置放在下一次MainRunloop的运行中,这样界面的跳转会很流畅。
*/
- (void)viewDidLoad
{
[super viewDidLoad];
dispatch_async(dispatch_get_main_queue(), ^{
/* alphaButton */
self.alphaButton = [[UIButton alloc] init];
self.alphaButton.frame = CGRectMake((V_width - 100*scale)/2, 100*scale, 100*scale, 100*scale);
self.alphaButton.backgroundColor = [UIColor redColor];
self.alphaButton.alpha = 0.5;
[self.alphaButton setTitle:@"透明按钮" forState:UIControlStateNormal];
[self.view addSubview:self.alphaButton];
}):
}
(5.)界面的数据采用异步线程的方式去计算配置,当界面数据都配置完全了,在回到主线程中去设置UI
(6.)在很多时候界面的数据我会需要从网络中获取,而有时多个网络请求之间没有关联关系,我们可以采用信号量的方式,去同步请求网络数据,当所有网络数据都返回后,在开始计算配置数据
/* 创建信号量 */
dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);
dispatch_async(dispatch_get_global_queue(0, 0), ^{
//这里面是网络请求1
//请求成功或者失败后需要去发送信号量,告诉等待队列已经完成一个任务的等待
dispatch_semaphore_signal(semaphore);
});
dispatch_async(dispatch_get_global_queue(0, 0), ^{
//这里面是网络请求2
//请求成功或者失败后需要去发送信号量,告诉等待队列已经完成一个任务的等待
dispatch_semaphore_signal(semaphore);
});
dispatch_async(dispatch_get_global_queue(0, 0), ^{
//这里面是网络请求3
//请求成功或者失败后需要去发送信号量,告诉等待队列已经完成一个任务的等待
dispatch_semaphore_signal(semaphore);
});
/* 有几个任务就创建对少个信号等待 */
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
/* 当网络数据都返回了,异步去配置计算界面最终显示需要的数据 */
dispatch_async(dispatch_get_global_queue(0, 0), ^{
//配置计算界面最终显示需要的数据
//数据配置完成后回到主线程更新UI
dispatch_async(dispatch_get_main_queue(), ^{
//更新UI
});
});
(7.)通过Storyboard创建的视图对象消耗的资源比纯代码创建对象要多很多
(8.)Block回调来异步执行任务回调(Block是个很神奇的东西,要灵活应用啊)
//文本的宽高计算会占用很大一部分资源,所以尽量用异步线程去执行操作,计算好后再回到主线程返回数据。
/**
计算文字宽高
@param string (NSString *) 计算高度的字符串
@param maxHeight (CGFloat) 最大高度[如果最大高度为0,表示无高度限制]
@param maxWidth (CGFloat) 最大宽度
@param textFont (UIFont *) 文字粗细度
@param block (CGSize) 返回文字的size
*/
+(void)textBoundingRectWithString:(NSString *)string maxHeight:(CGFloat)maxHeight maxWidth:(CGFloat)maxWidth textFont:(UIFont *)textFont Block:(void (^)(CGSize obj))block
{
/* 如果传入内容有误,直接返回结果到当前线程*/
if (!textFont || [self isBlankString:string] == YES) {
if (block) {
block(CGSizeMake(0, 0));
}
return;
}
/* 异步执行计算操作*/
dispatch_async(dispatch_get_global_queue(0, 0), ^{
CGSize lastSize;
if (maxHeight == 0) {
CGSize size = [string boundingRectWithSize:CGSizeMake(maxWidth, CGFLOAT_MAX) options:NSStringDrawingUsesLineFragmentOrigin attributes:@{NSFontAttributeName:textFont} context:nil].size;
lastSize = CGSizeMake(ceilf(size.width), ceilf(size.height));
}else
{
CGSize size = [string boundingRectWithSize:CGSizeMake(maxWidth, maxHeight) options:NSStringDrawingUsesLineFragmentOrigin attributes:@{NSFontAttributeName:textFont} context:nil].size;
lastSize = CGSizeMake(ceilf(size.width), ceilf(size.height));
}
/* 计算完成后再主线程中回调数据,因为一般拉倒值后会直接设置UI控件属性。 */
dispatch_async(dispatch_get_main_queue(), ^{
if (block) {
block(lastSize);
}
});
});
}
(9.)关于TableView的优化请看我另外一篇文章UITableView的性能优化
(10.)有次跟朋友讨论优化的时候,说道为什么微博内容多也复杂,流畅度这么高。我们改用的方法都用了,但是cell内部内容一复杂帧数就开始下降了。后来才知道,原来是自动布局的锅,再加上自己对文本内容认识深度不够。布局是非常好性能资源的,有时为了性能少用Autolayouer技术和UILabel(但实际情况好像不可能,哇咔咔)。那么选择一个好的自动布局第三方尤为重要了。微博可能是有一套非开源的布局方法吧(这里有个来自百度知道团队的开源项目可以看看代码学习学习:FDTemplateLayoutCell。)
(11.)图片的缩放,UIImageView的尺寸最好跟Bundle里的原图大小,因为图片的缩放是非常耗性能的。在实际开发中,需要适配不同的屏幕尺寸,这个时候就需要与设计大神们好好沟通了。我们常在开发适配的时候,会写一个比例尺寸,界面在不同屏幕下的尺寸都是按照这个比例缩放的。所以要把自己的比例告诉设计大神们才能达到不缩放。
/* 这是我常用的比例 */
#define scale MIN([UIScreen mainScreen].bounds.size.width, [UIScreen mainScreen].bounds.size.height)/375.f
如果还是会缩放,那么你就需要异步去把图片绘制成UIImageView大小的图片了
/**
根据边距拉伸图片
@param sourceImage 原图片
@param edgeInsets 边距
@param resizingMode 缩放模式
@param size 需要拉伸的大小
@param block 处理后的图片
*/
+(void)imageCompress:(UIImage *)sourceImage forEdgeInsets:(UIEdgeInsets)edgeInsets resizingMode:(UIImageResizingMode)resizingMode forSize:(CGSize)size Block:(void (^)(UIImage *image))block
{
/*异步处理*/
dispatch_async(dispatch_get_global_queue(0, 0), ^{
UIImage *Image;
Image = [sourceImage resizableImageWithCapInsets:edgeInsets resizingMode:resizingMode];
UIGraphicsBeginImageContext(CGSizeMake(size.width, size.height));
[Image drawInRect:CGRectMake(0,0,size.width, size.height)];
UIImage* newImage = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
if (block) {
/* 回到主线程 */
dispatch_async(dispatch_get_main_queue(), ^{
block(newImage);
});
}
});
}
(12.)避免不必要的图片缓存:通常我们会用imageNamed:来加载图片,但是这个方法会对图片进行缓存。对于一些只有特定界面才有不常用的图片用这个方法会造成一定的内存消耗,一般不常用的图片采用initWithContentsOfFile:。可以自己写一个UIImage的类别,自行判断使用哪一个方法。这个方法我有用过,但可能目前处理器性能太好或者设计同学的图片本身就很小,内存上并看不出多大差别。对于那些图片为主的App这个方法还是很有用的
(13.)减少文件读取次数:文件的读取是是否消耗资源的,所以在没有必要分开文件内容的情况下,尽量把内容放在一个文件中,减少消耗。例如图片的读取,第一种,多个标签图片放在一个图片中,然后根据图片进行区域绘制,这样就减少了对图片的读取时消耗CPU的性能,第二种shouldRasterize光栅化,在GPU渲染时,直接取出上次的绘制内容,来减少文件的读取和重新绘制。
标签:dispatch 缓存 cin uiimage 图片缩放 dev form 快速 图形
原文地址:https://www.cnblogs.com/moondev/p/10201669.html