ruanpapa和又吉君写字的地方


  • 首页

  • 分类

  • 归档

  • 标签
ruanpapa和又吉君写字的地方

ruanpapa和又吉君的日常之二

发表于 2017-04-10 | 分类于 ruanpapa&又吉君的日常 | | 阅读次数

ruanpapa和又吉君的日常之二

清明节放假,又吉君就算计着和小伙伴出去玩,去哪儿呢,不如:烟花三月下扬州 吧!

谁想到这一玩ruanpapa就不高兴了

前段时间还说又吉君是“小肾脏 ”的papa,竟然发小脾气了!

特别的昵称

特别的昵称


他发什么脾气呢

(´・ω・`)

(´・ω・`)



  • 对,就是因为老子出去玩了三天两天半没搭理他……这货无聊得要爆炸!

  • 嗯,于是他就发脾气了!

  • 发脾气了!

  • 脾气了!

  • 气了!

  • 了!

  • !

  • ……

    ​

    papa啊,我的papa。 你鸡母鸡啊,你每次有小脾气我都蛮担心的!我怕你一不小心就不理我了……但是我又觉得你不会不理我…很矛盾的心理(:3 」∠)然后我想讨好你吧,又不知道我自己哪里不对……

    所以!你可不可以下次不要发脾气呀!有不开心的事要和我讲噢!我好好听着!!

最后放几张扬州的图充充篇幅….

不知道哪个巷子里的路灯...

不知道哪个巷子里的路灯…





何园的亭子

何园的亭子





桃花

桃花





长长的夹道

长长的夹道





- 我是又吉君,一个集ruanpapa和好脾气于一身的大可爱!





by:又吉君

ruanpapa和又吉君写字的地方

PPRoundedAvatar--高性能的异步裁剪圆角头像控件

发表于 2017-04-01 | 分类于 ruanpapa--技术贴 | | 阅读次数

起因

最近的开发任务是进行性能优化,主要是提升列表(UITableView)的滚动流畅性。

在完成了提前算高、子视图层级优化几个优化步骤之后,滚动的流畅性已经有了明显的提升,平均的帧速率(fps)已经从优化之前49提高到了55。接下来通过 Instruments 的 Time Profile 工具分析,发现圆角头像的裁剪竟然占去了20%+的用户运行时间!图片的处理需要 GPU 和 CPU 的配合,本来就需要大量的处理时间,这本无可厚非。但是这么重的任务都派发到主线程肯定是会造成界面的卡顿的,而且圆角头像在项目中的使用星罗棋布,很多界面中都用到了这个控件,所以呢,对圆角头像的优化就很有必要了。

经过

优化性能,无非是将主线程从繁重的任务中解放出来,将能在后台线程完成的任务都派发到后台线程中。这里选用 NSOperation 进行多线程处理,因为 CoreGraphics 都是线程安全的,于是把图片处理(裁圆角/加边框)的过程都在后台线程中执行好,再在主线程设置图片。接下来说一下核心的技术点:

图片处理

我将图片裁剪的过程封装并暴露了两个便利接口,默认的 - (nullable UIImage )pp_imageByRoundCornerRadius: scaleSize: 会先将图片缩放到scaleSize的大小,再添加上圆角(注意默认的方法是没有边框的),另一个接口 - (nullable UIImage )pp_imageByRoundCornerRadius: scaleSize: borderWidth: borderColor: 则可以在默认方法的基础上设置边框颜色和边框的宽度。这里没有新建一个专门的处理类,而是拓展了 UIImage 类,具体的头文件如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
@interface UIImage (PPRoundedAvatar)
/**
将图片进行圆角处理,默认无边框
*/
- (nullable UIImage *)pp_imageByRoundCornerRadius:(CGFloat)radius
scaleSize:(CGSize)newSize;
/**
将图片进行圆角处理,并加上边框
*/
- (nullable UIImage *)pp_imageByRoundCornerRadius:(CGFloat)radius
scaleSize:(CGSize)newSize
borderWidth:(CGFloat)borderWidth
borderColor:(nullable UIColor *)borderColor;
/**
图片加上圆形边框,图片必须得是正方形的,否则直接返回未加边框的原图片
*/
- (nullable UIImage *)pp_imageByRoundBorderedColor:(nullable UIColor *)color
borderWidth:(CGFloat)width;
@end

图片进行圆角裁剪和加上边框主要还是采用 CoreGraphics 和 UIBezierPath 的方法,这里按下不表。值得注意的是处理的顺序应该是先缩放图片,再进行裁剪,不然边框的宽度会因为缩放而改变。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
// 图片圆角裁剪
- (UIImage *)pp_imageByRoundCornerRadius:(CGFloat)radius
borderWidth:(CGFloat)borderWidth
borderColor:(UIColor *)borderColor
{
UIImage *scaledImage = [self pp_imageScaledAspectToFillSize:newSize]; // 缩放图片
UIGraphicsBeginImageContextWithOptions(scaledImage.size, NO, 0);
CGContextRef context = UIGraphicsGetCurrentContext();
CGRect rect = CGRectMake(0, 0, scaledImage.size.width, scaledImage.size.height);
CGContextScaleCTM(context, 1, -1);
CGContextTranslateCTM(context, 0, -rect.size.height);
CGFloat minSize = MIN(scaledImage.size.width, scaledImage.size.height);
if (borderWidth < minSize / 2) {
UIBezierPath *path = [UIBezierPath bezierPathWithRoundedRect:CGRectInset(rect, borderWidth, borderWidth) byRoundingCorners:corners cornerRadii:CGSizeMake(radius, borderWidth)];
CGContextSaveGState(context);
[path addClip];
CGContextDrawImage(context, rect, scaledImage.CGImage);
CGContextRestoreGState(context);
}
UIImage *image = UIGraphicsGetImageFromCurrentImageContext();
image = [image pp_imageByRoundBorderedColor:borderColor borderWidth:borderWidth]; // 加上边框
UIGraphicsEndImageContext();
return image;
}
// 图片加边框
- (UIImage *)pp_imageByRoundBorderedColor:(UIColor *)borderColor
borderWidth:(CGFloat)borderWidth
{
if (self.size.height != self.size.width) {
return self;
}
if (!borderColor || borderWidth > self.size.width / 2 || borderWidth < 0) {
return self;
}
UIGraphicsBeginImageContextWithOptions(self.size, NO, 0);
[self drawAtPoint:CGPointZero];
CGRect rect = CGRectMake(0, 0, self.size.width, self.size.height);
CGFloat strokeInset = borderWidth / 2;
CGRect strokeRect = CGRectInset(rect, strokeInset, strokeInset);
CGFloat radius = self.size.width / 2;
UIBezierPath *path = [UIBezierPath bezierPathWithRoundedRect:strokeRect byRoundingCorners:UIRectCornerAllCorners cornerRadii:CGSizeMake(radius, borderWidth)];
path.lineWidth = borderWidth;
[borderColor setStroke];
[path stroke];
UIImage *result = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
return result;
}

圆角头像控件

这里采用 NSOperation + NSOperationQueue 的方式进行多线程处理,在图片、边框等属性的 set 方法里调用 setNeedsLayout 方法刷新布局,同时设置 _isNeedTransform 标记位为 YES,表示需要刷新,可以提高性能。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
- (void)setAvatarImage:(UIImage *)avatarImage
{
if (_avatarImage != avatarImage) {
_avatarImage = avatarImage;
_isNeedTransform = YES; // 需要刷新的标识
[self setNeedsLayout];
}
}
- (void)setBorderHidden:(BOOL)borderHidden
{
if (_borderHidden != borderHidden) {
_borderHidden = borderHidden;
_isNeedTransform = YES; // 需要刷新的标识
[self setNeedsLayout];
}
}
+ (NSOperationQueue *)sharedTransformQueue
{
static NSOperationQueue *transformQueue;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
transformQueue = [[NSOperationQueue alloc] init];
transformQueue.name = @"io.github.vernonvan.PPRoundedAvatar.sharedOperationQueue";
transformQueue.maxConcurrentOperationCount = 20;
});
return transformQueue;
}
- (void)layoutSubviews
{
[super layoutSubviews];
if (!self.avatarImage && !self.imageBackgroundColor) {
return;
}
if (self.bounds.size.width <= 0 || self.bounds.size.height <= 0) {
return;
}
if (_isNeedTransform || !CGSizeEqualToSize(self.bounds.size, self.imageView.image.size)) {
[self transformImage];
}
}
- (void)transformImage
{
UIImage *startImage = self.avatarImage;
NSBlockOperation *transformOperation = [[NSBlockOperation alloc] init];
__weak NSBlockOperation *weakTransformOperation = transformOperation;
[transformOperation addExecutionBlock:^{
NSBlockOperation *strongTransformOperation = weakTransformOperation;
if ([strongTransformOperation isCancelled]) {
return;
}
UIImage *transformedImage = nil;
if (self.avatarImage) {
transformedImage = [self.avatarImage pp_imageByRoundCornerRadius:self.bounds.size.width scaleSize:self.bounds.size];
} else if (self.imageBackgroundColor) {
transformedImage = [UIImage pp_roundImageWithColor:self.imageBackgroundColor radius:self.bounds.size.width / 2];
}
if (!self.borderHidden) {
transformedImage = [transformedImage pp_imageByRoundBorderedColor:self.borderColor borderWidth:self.borderWidth];
}
dispatch_async(dispatch_get_main_queue(), ^{
if ([strongTransformOperation isCancelled]) {
return;
}
if (self.avatarImage == startImage) { // 1
_isNeedTransform = NO;
[self setImage:transformedImage forState:UIControlStateNormal];
}
});
}];
[[self.class sharedTransformQueue] addOperation:transformOperation];
}

在上面有一个标注了1的注释点,这里的条件判断是为了避免多线程时序性的问题而加的。考虑这样的一种常见情况:圆角头像控件是表格上的单元格(cell)上的子视图,某个 cell 被滑到屏幕上,于是开始头像的裁剪(这里将这个头像称为旧头像),然后在这个裁剪尚未完成的时候,这个 cell 被滑出了屏幕,然后根据新的图片裁剪圆角(这个头像称为新头像),可能出现新头像裁剪早于旧头像完成的情况,就会导致控件先设置头像为新头像,然后被慢悠悠才完成裁剪的旧头像覆盖的问题。所以这里用这个条件避免这个问题。

结果

将上述的操作都封装隐藏好,现在的圆角头像控件 PPRoundedAvatar 的使用就很简单了,

1
2
3
4
5
6
7
PPRoundedAvatar *avatar = [[PPRoundedAvatar alloc] initWithFrame:CGRectMake(0, 0, 100, 100)];
avatar.avatarImage = [UIImage imageNamed:@"example.png"]; // 头像图片
avatar.borderWidth = 1.0; // 边框宽度
avatar.borderColor = UIColor.blackColor; // 边框颜色
avatar.borderHidden = NO; // 显示边框
avatar.imageBackgroundColor = UIColor.grayColor; // 背景颜色
[self.view addSubview:avatar];

具体的代码已经丢到了 github 上了,同时支持 Cocoapods 导入项目。
有需要的可以 clone 进行使用,也欢迎 pull request 完善这个控件。

插叙

技术上的问题就说到了这里,可是做一个开源项目不仅在技术上需要反复斟酌,而且在 github 的展示、demo 的展示等地方都需要有界面的设计,可苦了我这直男审美……/(ㄒoㄒ)/~~
这也导致了我家又吉君实在是看不下去了,终于勇敢地站了出来帮我把界面的设计给做好了,当中的你来我往又是另外的故事。真的是对我家又吉君感激涕零~~~
呐,又吉君,你帮我画一辈子设计好不好吖,我带你去吃鸡锁骨、卷饼、酱香饼、枣糕还有烤冷面哦~哈哈哈哈啊哈哈哈

ruanpapa和又吉君写字的地方

ruanpapa和又吉君的日常之一

发表于 2017-04-01 | 分类于 ruanpapa&又吉君的日常 | | 阅读次数

ruanpapa和又吉君的日常

ruanpapa最近做了个开源的圆角裁剪工具,期间那是被我各种嫌弃啊!代码放一边不说,反正是开源的,大家拿去用用看就行。

但是!!!里面的各种展示图片简直!

丑!!很丑!!没有更丑!!

于是…..

  • “ruanpapa!!!!啊啊啊啊!你看看你这个线怎么像猪八戒的耙子!!”
  • “你不能把这个弄成圆角么!!”
  • “啊!!!你这个选图不行啊!头像图片选的颜色太杂了!”
  • “你这边框,这么细,加了和没加有什么区别!”
  • ……..
    能怎么办

    能怎么办



万般无奈之下,又吉君开始了“设计之旅”

于是…..
这张充满了直男癌风格的展示图就可爱了!

喏,展示图

喏,展示图




还有经过多次失败弄出来的图标…

么么扎٩(๑❛ᴗ❛๑)۶

么么扎٩(๑❛ᴗ❛๑)۶



  • 啊哈哈哈哈哈哈哈哈哈!!!实际上我ruanpapa设计的也挺好看的!但是我就是喜欢我弄的!!!!
  • 我是又吉君,一个集ruanpapa和智慧于一身的大可爱!
  • 嚯嚯嚯嚯嚯嚯嚯!!



    by:又吉君

ruanpapa和又吉君写字的地方

ruanpapa和又吉君的日常之零

发表于 2017-03-30 | 分类于 ruanpapa&又吉君的日常 | | 阅读次数

ruanpapa和又吉君到底是什么鬼?

  • ruanpapa,多年草本植物,性温和,不易上火,无毒无公害。植株上长着不明自来卷毛发,无花期,只结果。茎干呈黑黄,估计大晚上的容易被踩到。
  • 又吉君,小型食肉动物,性情多变,不易有攻击行为,嗜睡,好吃,眼睛白长了。喜欢晒太阳,毛发蓬松,天气干燥易炸毛,皮肤黄里透白,太阳光下会发光。

两货怎么认识的

  • 有一天晚上又吉君出门捕食,走了十公里遇到了这棵草,不留神就踩到了。于是这棵草就赖上又吉君了。时不时给懒惰的又吉君投个食,于是又吉君就更懒惰了。

相处模式

  • -“宝宝,给我个亲亲”
    -“拒绝”
    -“不给?????!那好吧……”

  • -“papapapa,帮我找个软件/教程”
    -“好的!٩(๑❛ᴗ❛๑)۶”

  • -“papa!你这是什么鬼!丑死
    了!!!!!!!!!”
    -“…….”

  • -“宝宝,我们睡觉吧”
    -“睡你麻痹!起来嗨”

  • -“papa,我好困(´・ω・`)”
    -“这就困了??”
    “……..”

    “宝宝???”

    “妈的!睡着了!!!?”

实际情况

  • 知乎相识
  • 认识三个月
  • 五一去旅游
  • …….

未来那么长,请多多指教啊!

嘿,旁边的傻papa

嘿,旁边的傻papa




by:又吉君

​

ruanpapa和又吉君写字的地方

Swift的lazy关键字–延迟加载

发表于 2016-12-28 | 分类于 ruanpapa--技术贴 | | 阅读次数

定义

lazy属性就是初始值直到第一次使用的时候才执行计算的属性,这对小内存的手机所产生的性能上的优化是相当可观的。
注意:lazy属性必须是变量(var修饰符),因为常量属性(let修饰符)必须在初始化之前就有值,所以常量属性不能定义为lazy。

Objective-C中的延迟加载

Objective-C并没有在语法上支持延迟加载,通常是由程序员自己手动实现的。
示例如下:

1
2
3
4
5
6
7
8
9
@property (nonatomic, strong) NSArray *names;
- (NSArray *)names {
if (!_names) {
_names = [[NSArray alloc] init];
NSLog(@"只在首次访问输出");
}
return _names;
}

说明:在初始化对象后,_names 是 nil。只有当首次访问 names 属性时 getter 方法会被调用,并检查如果还没有初始化的话,就进行赋值。可以想见,控制台打印的“只在首次访问输出”的确只会输出一次。我们之后再多次访问这个属性的话,因为 _names已经有值,因此将直接返回。
分析:getter方法和下划线语法对初学者并不是那么的友好,同时属性以及对应的getter方法空间上隔得比较远,代码逻辑不直观。

Swift的延迟加载

Swift中则可以通过lazy关键字简单地实现相同功能,比如上述示例代码在Swift中实现的话:

1
2
3
4
5
lazy var names: NSArray = {
let names = NSArray()
print("只在首次访问输出")
return names
}()

分析:相比起Objective-C中的实现,现在的lazy是在是简单的多了,而且更加的直观。除了上述直接在属性后面定义闭包调用的方法以外,还可以使用实例方法(func)和类方法(class func)来为lazy属性初始化添加必要的逻辑。

使用场景

延迟加载主要有以下两个使用的场景:

  1. 属性的初始值依赖于其他的属性值,只有其他的属性值有值之后才能得出该属性的值。
  2. 属性的初始值需要大量的计算。

高级用法

在Swift标准库中还有一组lazy方法,可以配合map、filter这类接受闭包并进行运行的方法一起,让整个行为变成延时进行的。以下是map函数的用法(示例取自喵神的Swifter):

1
2
3
4
5
6
7
8
9
10
11
12
13
let data = 1...3
let result = data.lazy.map {
(i: Int) -> Int in
print("正在处理 \(i)")
return i * 2
}
print("准备访问结果")
for i in result {
print("操作后结果为 \(i)")
}
print("操作完毕")

1
2
3
4
5
6
7
8
// 准备访问结果
// 正在处理 1
// 操作后结果为 2
// 正在处理 2
// 操作后结果为 4
// 正在处理 3
// 操作后结果为 6
// 操作完毕

对于那些不需要完全运行,可能提前退出的情况,使用 lazy 来进行性能优化效果会非常有效。

参考链接:

  1. 喵神:LAZY 修饰符和 LAZY 方法
  2. Swiftist:Swift中的延迟加载
  3. Apple developer
123
ruanpapa & 又吉君

ruanpapa & 又吉君

ruanpapa和又吉君写字的地方

25 日志
3 分类
12 标签
GitHub
© 2018 ruanpapa & 又吉君
由 Hexo 强力驱动
主题 - NexT.Muse