0%

beginTime 的妙用

创建一个动画,添加到不同的图层上,可以实现复用,通过调节 beginTime 可以调节动画开始执行的时间。

例如,我们创建一个水平位移动画,添加到不同的图层对象上,并且让他们轮流执行。

阅读全文 »

使用场景

给视图对象快速创建约束,可以使用比较冷门的 VFL(Visual Format Language),本质是是基于自动布局(AutoLayout)。

阅读全文 »

前言

2016 对于我来说的意义,就像 10 年前的今天, iPhone 之余乔布斯, iPhone 发布后 10 年后的今天,小程序之余张小龙。它跨越了一道深深的鸿沟,并向自己理想的方向坚定前行。

一直相信努力就会有回报,付出就会有收获,是金子总会发光。

  • 2016 是我加入客路一周年的时间,认识了很多有趣的朋友和同事的一
  • 2016 是我迈出自我,走出中国探索未知的世界的一年。
  • 2016 是旅行,工作,生活,学习,社交一样都没落下的一年。

曾经的铁哥们也即将回归东莞,这样我们的距离又进了一步,以后有更多的时间聊理想谈人生,想想突然好激动。

阅读全文 »

获取设备音量

播放音频可以通过:

1
2
MPMusicPlayerController *iPod = [MPMusicPlayerController systemMusicPlayer];
float volumeLevel = iPod.volume;

播放视频可以通过:

1
float outputVolume = [[AVAudioSession sharedInstance] outputVolume];

推荐下面的方法,上面的在某些版本可能有问题,下面的方法兼容iOS6 及以上

静音模式失效

通过设置音频会话的 category 实现:

1
2
3
4
5
NSError *setCategoryError = nil;
BOOL success = [[AVAudioSession sharedInstance] setCategory: AVAudioSessionCategoryPlayback
error: &setCategoryError];

if (!success) { /* handle the error in setCategoryError */ }

这样 App 就不会随着手机静音键打开而静音,可在手机静音下播放声音😁

监听音量改变

监听音频改变私有通知:

1
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(volumeChanged:) name:@"AVSystemController_SystemVolumeDidChangeNotification" object:nil];

实现通过回调:

1
2
3
4
5
6
7
- (void)volumeChanged:(NSNotification *)notification
{
float volume = [[[notification userInfo] objectForKey:@"AVSystemController_AudioVolumeNotificationParameter"]
floatValue];

// do something
}

设置设备音量

使用 MPVolumeView 类,便利它的子 views 找到类为 MPVolumeSlider 的滑竿。

1
2
3
4
5
6
7
8
MPVolumeView *volumeView = [[MPVolumeView alloc] init];
UISlider *volumeViewSlider = nil;
for (UIView *view in [volumeView subviews]){
if ([view.class.description isEqualToString:@"MPVolumeSlider"]) {
volumeViewSlider = (UISlider *)view;
break;
}
}

然后再通过设置 volumeViewSlidervalue 即可。

1
_volumeViewSlider.value = someVolume;

监听静音按钮

参考Sound Switch - Sharkfood的实现。

使用很简单,判断是否为静音模式:

1
2
3
if ([SharkfoodMuteSwitchDetector shared].isMute) {
// do something
}

动态监听,通过 block 回调:

1
2
3
[SharkfoodMuteSwitchDetector shared].silentNotify = ^(BOOL silent){
// do something
};

监听耳机拔插

监听 AVAudioSessionRouteChangeNotification 通知:

1
2
3
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(audioRouteChangeListenerCallback:) name:AVAudioSessionRouteChangeNotification
object:nil];

实现回调:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
- (void)audioRouteChangeListenerCallback:(NSNotification*)notification
{
NSDictionary *interuptionDict = notification.userInfo;

NSInteger routeChangeReason = [[interuptionDict valueForKey:AVAudioSessionRouteChangeReasonKey] integerValue];
switch (routeChangeReason) {
case AVAudioSessionRouteChangeReasonNewDeviceAvailable:
// 耳机插入
break;

case AVAudioSessionRouteChangeReasonOldDeviceUnavailable:
// 耳机拔掉
break;

case AVAudioSessionRouteChangeReasonCategoryChange:
// called at start - also when other audio wants to play
NSLog(@"AVAudioSessionRouteChangeReasonCategoryChange");
break;
}
}

前言

UIImageView 应该是iOS中使用最频换的控件,就如日常吃饭一样,天天都在重复,有时或许应该反思一下,怎么使用这个控件,达到低能耗,最佳用户体验。

针对单张图片来说,常见的处理是在图片准备显示时增加一个淡出动画,能使图片显示闲的很平滑。

多张图片也一样,在第一张图片的基础上淡出原来的图片,淡入新的图片。也可以说是溶解效果。

很多人喜欢对图片的 alpha 做淡出动画,使 alpha 从 0 到 1 动画改变。这种动画有一点不好的是,在动画结束后,图片会明显的出现一闪,这样使动画看起来有点突兀。比较好的做法时,在将要显示时给图片做一个转场动画。

淡出动画实现

下面是其中一种简单的实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
@implementation UIImageView (RFWebImage)

- (void)animatedChangeToImage:(UIImage *)img
{
[UIView transitionWithView:self
duration:0.3f
options:UIViewAnimationOptionTransitionCrossDissolve
animations:^{
self.image = img;
} completion:NULL];
}

@end

思路:在 ImageView 将要显示是使用转场动画函数来实现淡出动画效果,体验应该是是各种动画中最好的了,而且使用起来很简单。

在淡出显示的动画基础上,我们引出今天的主角,动画切换 Image。

思路:单张图片淡出我们已经实现,现在做的就是在切换一张新的图片时同时再加入淡出或者说溶解效果即可。

动画切换 Image

比较常见的有下面 3 种实现:

  • CATransition 类实现
  • UIView 动画转场 API 实现
  • CABasicAnimation 类实现

CATransition 实现

CATransition 类是iOS中很好用的控制转场动画的类,通过简单的配置可以实现常见而炫酷的动画效果,变换类型通过 type 字段控制, subtype 可以很细化控制动画的方向(比如动画开始的上下左右方向)。 CATransition 继承至 CAAnimation 可以对动画设置动画曲线(timingFunction),可以通过代理获取动画状态(是已经开始,还是已经停止,已经是否完成)。

type 支持四种类型:

  • kCATransitionFade // 淡入淡出
  • kCATransitionMoveIn // 从某个方向向终点平移知道覆盖在上方
  • kCATransitionPush // 把原来的推出去,自己推出去
  • kCATransitionReveal // 把原来的从正上方解开,自己在下面

下面是样板代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
- (void)animatedSwichImageMethodOne {

UIImage *toImage = [self getRadomImage];

CATransition *transition = [CATransition animation];
transition.duration = 0.3f;
transition.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut];
transition.type = kCATransitionFade;
transition.subtype = kCATransitionFromTop;

[self.imageViewOne.layer addAnimation:transition forKey:nil];
[self.imageViewOne setImage:toImage];
}

UIView 动画转场

1
+ (void)transitionWithView:(UIView *)view duration:(NSTimeInterval)duration options:(UIViewAnimationOptions)options animations:(void (^ __nullable)(void))animations completion:(void (^ __nullable)(BOOL finished))completion NS_AVAILABLE_IOS(4_0);

通过上面的函数实现,其实是对第一种的高级封装。通过设置 optionsUIViewAnimationOptionTransitionCrossDissolve 即可。

下面是样板代码:

1
2
3
4
5
6
7
8
9
10
11
- (void)animatedSwichImageMethodTwo {

UIImage *toImage = [self getRadomImage];

[UIView transitionWithView:self.imageViewTwo
duration:0.3f
options:UIViewAnimationOptionTransitionCrossDissolve | UIViewAnimationOptionCurveEaseInOut
animations:^{
self.imageViewTwo.image = toImage;
} completion:nil];
}

CABasicAnimation 实现

CABasicAnimation 是核心动画一个重要的类,继承至 CAPropertyAnimation ,可以对所有的可动画属性做动画,可以通过 fromValuetoValuebyValue 字段控制动画的进度。
在这里我们是对 CALayercontents 属性做动画,在改变图片时,创建一个 CABasicAnimation 对象添加到 ImageView 的图层上即可。

下面是样板代码:

1
2
3
4
5
6
7
8
9
10
11
- (void)animatedSwichImageMethodThree {

UIImage *toImage = [self getRadomImage];

CABasicAnimation *animation = [CABasicAnimation animationWithKeyPath:@"contents"];
animation.toValue = toImage;
animation.duration = 0.3f;

[self.imageViewThree.layer addAnimation:animation forKey:@"contentsAnimationKey"];
[self.imageViewThree setImage:toImage];
}

更多内容请下载Demo查看(🤔Bonus: Flip 效果🤔

Toll-Free Bridged Types

Core Foundation 框架和 Foundation 框架中有很多数据类型可以交替转换。能够被交替转换的数据类型也被叫做 Toll-Free Bridged 数据类型。这意味着你能像参数一样使用相同的数据结构对一个 Core Foundation 的函数进行调用,或者像 Objective-C 的消息接受模式一样执行。例如, NSLocale (查看NSLocale Class Reference)可以与在 Core Foundation 中对应的 CFLocale (查看CFLocale Reference)之间互相转换。

不是所有数据类型都是 Toll-Free Bridged ,即使它们的名字可能让你认为它们是。例如, NSRunLoop 是没有对应的桥接类型 CFRunLoop , NSBundle 也没有对应的桥接类型 CFBundleNSDateFormatter 同样没有对应的桥接类型 CFDateFormatter

文章末尾表 1 提供了一份支持无缝桥接的数据类型的列表。

注意:假如你使用一个自定义回调在一个 Core Foundation 框架的集合中,包含一个 NULL 回调,当使用 Objective-C 的方式接入它,它的内存管理方式是未定义的。

类型转换和对象语义周期声明

通过无缝桥接技术,在一个你以 NSLocale * 做为一个参数的方法的例子中,你能传递一个 CFLocaleRef 结构体,并且当你看到有一个 CFLocaleRef 参数的函数中,你能够传递一个 NSLocale 实例对象。当然你也必须提供给编译器相关的一些其它信息:第一,你必须转换一种类型成其它;第二,你可能必须指明对象的语义生命周期。

编译器理解 Objective-C 的方法并且返回 Core Foundation 数据类型,下面是 Cocoa 命名转换的历史(查看Advanced Memory Management Programming Guide)。例如,编译器知道,在 iOS 中,通过 UIColorCGColor 方法返回的 CGColor 并不应该被持有。你必须使用恰当的类型转换,像下面例子演示的那样:

1
2
NSMutableArray *colors = [NSMutableArray arrayWithObject:(id)[[UIColor darkGrayColor] CGColor]];
[colors addObject:(id)[[UIColor lightGrayColor] CGColor]];

编译器并不会自动管理 Core Foundation 对象的生命周期。你必须告诉编译器对象的语义所属关系通过使用一种转换(定义在objc/runtime.h)或者 Core Foundation 风格的宏(定义在 NSObject.h):

  • __bridge 关键字表示转换指针在 Objective-CCore Foundation 之间而不会转换所属关系。
  • __bridge_retained 关键字或者 CFBridgingRetain 表示转换指针在 Objective-CCore Foundation 之间并且把所属权交给你。你负责调用 CFRelease 或者相关的函数来交出对象的所属权。
  • __bridge_transfer 关键字或者 CFBridgingRelease 表示转换一个非 Objective-C 的指针到 Objective-C 并且转换所属权给ARCARC负责交出对象的所属权。

下面是一些例子:

1
2
3
4
5
6
7
8
9
10
11
NSLocale *gbNSLocale = [[NSLocale alloc] initWithLocaleIdentifier:@"en_GB"];
CFLocaleRef gbCFLocale = (__bridge CFLocaleRef)gbNSLocale;
CFStringRef cfIdentifier = CFLocaleGetIdentifier(gbCFLocale);
NSLog(@"cfIdentifier: %@", (__bridge NSString *)cfIdentifier);
// Logs: "cfIdentifier: en_GB"

CFLocaleRef myCFLocale = CFLocaleCopyCurrent();
NSLocale *myNSLocale = (NSLocale *)CFBridgingRelease(myCFLocale);
NSString *nsIdentifier = [myNSLocale localeIdentifier];
CFShow((CFStringRef)[@"nsIdentifier: " stringByAppendingString:nsIdentifier]);
// Logs identifier for current locale

下面的例子显示了使用口述的 Core Foundation 内存管理规则来管理 Core Foundation 内存:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
- (void)drawRect:(CGRect)rect {
CGContextRef ctx = UIGraphicsGetCurrentContext();
CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceGray();
CGFloat locations[2] = {0.0, 1.0};
NSMutableArray *colors = [NSMutableArray arrayWithObject:(id)[[UIColor darkGrayColor] CGColor]];
[colors addObject:(id)[[UIColor lightGrayColor] CGColor]];
CGGradientRef gradient = CGGradientCreateWithColors(colorSpace, (__bridge CFArrayRef)colors, locations);
CGColorSpaceRelease(colorSpace); // Release owned Core Foundation object.

CGPoint startPoint = CGPointMake(0.0, 0.0);
CGPoint endPoint = CGPointMake(CGRectGetMaxX(self.bounds), CGRectGetMaxY(self.bounds));
CGContextDrawLinearGradient(ctx, gradient, startPoint, endPoint,
kCGGradientDrawsBeforeStartLocation | kCGGradientDrawsAfterEndLocation);
CGGradientRelease(gradient); // Release owned Core Foundation object.
}

无缝桥接类型

下面的表格提供了一个在 Core FoundationFoundation 中可以交替转换数据类型列表。对每一对桥接类型,表也列举出了这些无缝桥接类型在 OS X 中的可用版本。

Core Foundation 类型 Foundation 类型 可用性
CFArrayRef NSArray OS X v10.0
CFAttributedStringRef NSAttributedString OS X v10.4
CFCalendarRef NSCalendar OS X v10.0
CFCharacterSetRef NSCharacterSet OS X v10.4
CFDataRef NSData OS X v10.0
CFDateRef NSDate OS X v10.4
CFDictionaryRef NSDictionary OS X v10.0
CFErrorRef NSError OS X v10.4
CFLocaleRef NSLocale OS X v10.0
CFMutableArrayRef NSMutableArray OS X v10.4
CFMutableAttributedStringRef NSMutableAttributedString OS X v10.0
CFMutableCharacterSetRef NSMutableCharacterSet OS X v10.4
CFMutableDataRef NSMutableData OS X v10.0
CFMutableDictionaryRef NSMutableDict OS X v10.4
CFMutableSetRef NSMutableSet OS X v10.0
CFMutableStringRef NSMutableString OS X v10.4
CFNumberRef NSNumber OS X v10.0
CFReadStreamRef NSInputStream OS X v10.4
CFRunLoopTimerRef NSTimer OS X v10.0
CFSetRef NSSet OS X v10.4
CFStringRef NSString OS X v10.0
CFTimeZoneRef NSTimeZone OS X v10.4
CFURLRef NSURL OS X v10.0
CFWriteStreamRef NSOutputStream OS X v10.4

参考资料

前言

随著 blockiOS4.0OS X 10.6的引入,给事件传递一种新的方式实现,在开发中用得最多的场景莫过于事件回调。使用 block 相对与 delegate 的优势在于,业务集中,可读性强,代码内联,不像代理需要实现很多函数,在适当的场景选择这种方式实现事件传递或者传参效果非常好,现在很多开源项目都实现了两种方法的事件回调。

block 用起来虽然很爽,但也有它的不足,存在循环引用,轻者内存泄露,甚至导致App崩溃,不易调试追溯,因此使用它使一定要小心。鉴于实践中的踩过各种坑,总结下来,方便自己和他人以后查阅,这就是block备忘录写作的初衷。

block 的本质

block 实际上是指向结构体的指针,编译时, block 的内部代码生产对应的函数。

具体结构如下:

block 结构图

与 C 语言的函数指针的区别

  • block 的代码是内联的,效率高于函数调用
  • block 对于外部变量默认是只读属性
  • blockObjective-C 看成是对象处理

block 声明

  • 作为 property > @property (nonatomic, copy) returnType (^blockName)(parameterTypes);

  • 作为方法参数

    - (void)someMethodThatTakesABlock:(returnType (^)(parameterTypes))blockName;

  • 作为一个方法调用参数

    [someObject someMethodThatTakesABlock:^returnType (parameters) {…}];

  • 作为一个 typedef > typedef returnType (^TypeName)(parameterTypes);

    TypeName blockName = ^returnType(parameters) {…};

  • 作为函数参数

    int (^sumOfNumbers)(int a, int b) = ^(int a, int b) {
    return a + b;
    };

SDWebImage 中使用的 block 示例:

1
2
3
4
5
typedef void(^SDWebImageCompletionBlock)(UIImage *image, NSError *error, SDImageCacheType cacheType, NSURL *imageURL);

typedef void(^SDWebImageCompletionWithFinishedBlock)(UIImage *image, NSError *error, SDImageCacheType cacheType, BOOL finished, NSURL *imageURL);

typedef NSString *(^SDWebImageCacheKeyFilterBlock)(NSURL *url);

block 调用

跟 C 函数类似使用 () ,括号里面还可以带一个或者多个参数

1
2
3
4
5
6
7
8
9
10
// block 声明
void)(^loggerBlock)(void);

// block 定义
loggerBlock = ^{
NSLog("hello world")
};

// block 调用
loggerBlock();

block 内存管理

默认情况下, block 是在栈内存中,它不会对所引用的对象进行任何操作;如果对 block 进行一次 copy 操作, block 就会在堆内存中,并且它会它所有的引用的对象做一次 retain 操作

  1. 对于 block 外的变量引用,block 默认是将其复制到其数据结构中来实现访问的
  2. 对于用 __block 修饰的外部变量引用,block 是复制其引用地址来实现访问的
  3. 而 block 会捕获代码外的局部变量,并且仅限于只读操作
  4. 在 block 中希望修改的外界局部对象,必须加上 __block 关键词

ARC

如果对象使用 `__unsafe_unretained` 或 `__weak` 修饰,就不会对其做 `retain` 操作

MRC

如果对象使用了 `__block` 修饰, 就不会对其做 `retain` 操作

为了防止 block 中的循环引用,可以用 __weak 关键词把相应的对象声明为弱引用,在 block 快内部需要多次访问,防止该对象被释放,可以用 __strong 关键词将声明为强引用:

1
2
3
4
5
6
7
8
9
__weak __typeof__(self) weakSelf = self;

dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{

__strong __typeof(self) strongSelf = weakSelf;

[strongSelf doSomething];
[strongSelf doOtherThing];
});

参考链接

前言

此文是基于这些年工作中项目里面常见崩溃的一些总结,整理出来方便查阅,希望对大家都有所帮助。

App 常见崩溃

  1. 数组下标越界
  2. 字典构造与修改
  3. NSAttributedString 相关
  4. 呈现一个空控制器
  5. unrecognized selector
  6. 操作 tableView 数据
  7. Push 到同一个控制器多次

1.数组下标越界

示例代码

1
2
3
4
5
- (void)testArrayOutOfBounds
{
NSArray *testArray = @[@1,@2,@3];
NSNumber *num = testArray[3];
}

异常现象

Terminating app due to uncaught exception ‘NSRangeException’, reason: ‘** -[__NSArrayI objectAtIndex:]: index 3 beyond bounds [0 .. 2]’*

预防方案

在数组中取值时需要先进组下标索引边界检查,如果没有越界方可取值。

2.字典构造造与修改

示例代码

1
2
3
4
5
6
7
8
- (void)testDicSetNilValueCrash
{
// 构造不可变字典时 key 和 value 都不能为空
NSString *nilValue = nil;
NSString *nilKey = nil;
NSDictionary *dic1 = @{@"key" : nilValue};
NSDictionary *dic2 = @{nilKey : @"value"};
}

异常现象

Terminating app due to uncaught exception ‘NSInvalidArgumentException’, reason: ‘** -[__NSPlaceholderDictionary initWithObjects:forKeys:count:]: attempt to insert nil object from objects[0]’*

预防方案

在我们使用字面量快速创建一个字典的时候需要特别小心,因为很可能字典的键和值不能保证同时不为空。有潜在崩溃的风险,这种崩溃非常容易出现,需要特别小心,但是当你留心的话也非常好避免,就是设置字典的键或者值的时候判断是否非空,可变字典设置某个键的值是可以为空,相当于删除字典中的某个键值。为了使 App 保持健壮推荐使用 KVO 或者字面量的方式来设置字典的值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
- (void)testMutableDicSetNilValueCrash
{
NSString *value = nil;
NSMutableDictionary *mDic = [NSMutableDictionary dictionary];

// via Dic set, leading crash
[mDic setObject:value forKey:@"key"];

// via KVO set, it's safe
[mDic setValue:value forKey:@"key"];

// or via literal set, it's safe
mDic[@"key"] = value;
}

3.NSAttributedString 相关

示例代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
- (void)testAttributedStringInitCrash
{
NSString *nilStr = nil;
NSMutableAttributedString *attributedStr = [[NSMutableAttributedString alloc] initWithString:nilStr];
}

- (void)testAttributedStringAddAttributeCrash
{
NSString *nonnullStr = @"str";
NSMutableAttributedString *attributedStr = [[NSMutableAttributedString alloc] initWithString:nonnullStr];

NSString *nilValue = nil;
[attributedStr addAttribute:NSAttachmentAttributeName value:nilValue range:NSMakeRange(0, 1)];
}

异常现象

Terminating app due to uncaught exception ‘NSInvalidArgumentException’, reason: ‘NSConcreteMutableAttributedString initWithString:: nil value’

预防方案

在构造 NSMutableAttributedString 或者 NSAttributedString 需要留意,设置的属性的值是否有可能存在 nil 的情况。这个很容易被人忽视,值得注意。

4.呈现一个空控制器

示例代码

1
2
3
4
5
6
7
- (void)testPresentNilControllerCrash
{
UIViewController *someVC = [UIViewController new];
UIViewController *presentVC = nil;

[someVC presentViewController:presentVC animated:YES completion:nil];
}

异常现象

present 一个空的控制器导致 App crash

预防方案

present 一个新控制器时,判断是否存在,存在才执行,否则直接返回

1
2
3
4
5
6
7
8
9
- (void)testPresentNilControllerCrashFixed
{
UIViewController *someVC = [UIViewController new];
UIViewController *presentVC = [UIViewController new];

if (presentVC) {
[someVC presentViewController:presentVC animated:YES completion:nil];
}
}

5.unrecognized selector

示例代码

1
2
3
4
- (void)testUnrecogernizedSelectorCash
{
[self performSelector:@selector(testSel) withObject:nil afterDelay:0];
}

异常现象

Terminating app due to uncaught exception ‘NSInvalidArgumentException’, reason: ‘-[ViewController testSel]: unrecognized selector sent to instance 0x7ffd41609d10’

预防方案

此类崩溃经常出现,特别是当服务器数据放回异常时,比如本来应该返回一个 NSString 类型字符串,结果返回 NULL ,当你调用字符串的 length 方式时,导致 App 崩溃。预防方法,重要的地方对类型进行判断再调用该类的相关方法,或者写一个分类统一处理此类逻辑。

6.操作 tableView 数据

示例代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
- (void)testTableViewUpdateCrash
{
NSIndexPath *insertIndexPath = [NSIndexPath indexPathForRow:0 inSection:0];
NSIndexPath *deleteIndexPath = [NSIndexPath indexPathForRow:1 inSection:0];
NSIndexPath *reloadIndexPath = [NSIndexPath indexPathForRow:2 inSection:0];
NSIndexPath *moved1IndexPath = [NSIndexPath indexPathForRow:3 inSection:0];
NSIndexPath *moved2IndexPath = [NSIndexPath indexPathForRow:4 inSection:0];
[self.tableView beginUpdates];

[self.tableView insertRowsAtIndexPaths:@[insertIndexPath] withRowAnimation:UITableViewRowAnimationAutomatic];
[self.tableView deleteRowsAtIndexPaths:@[deleteIndexPath]withRowAnimation:UITableViewRowAnimationAutomatic];
[self.tableView reloadRowsAtIndexPaths:@[reloadIndexPath] withRowAnimation:UITableViewRowAnimationAutomatic];
[self.tableView moveRowAtIndexPath:moved1IndexPath toIndexPath:moved2IndexPath];

[self.tableView endUpdates];
}

异常现象

Fatal Exception: NSInternalInconsistencyException

Invalid update: invalid number of sections. The number of sections contained in the table view after the update (1) must be equal to the number of sections contained in the table view before the update (1), plus or minus the number of sections inserted or deleted (1 inserted, 0 deleted).

预防方案

当需要动态更新 tableView 的数据时,计算好模型的数据使模型的数据和更新 tableView 的后的数据保持同步。

7.Push 到同一个控制器多次

异常现象

Fatal Exception: NSInvalidArgumentException

Pushing the same view controller instance more than once is not supported (<PPSelectPayMethodViewControllerIOS7: 0x10d7e7f10>)

参考链接

以上就是工作中常见的异常崩溃以及处理方案,下面的异常分类内容来自 Apple 的官方文档,有兴趣的可以查阅。☕️

Apple 官方常见异常类型(Exception types)

  1. 访问一块坏内存 [EXC_BAD_ACCESS] // SIGSEGV // SIGBUS] 2. 异常退出 [EXC_CRASH // SIGABRT] 3. 追踪受限 [EXC_BREAKPOINT // SIGTRAP] 4. 非法指令 [EXC_BAD_INSTRUCTION // SIGILL] 5. 被保护的资源遭到侵害 [EXC_GUARD] 6. 资源限制 [EXC_RESOURCE] 7. 其他异常类型

1.访问一块坏内存(Bad Memory Access)

当程序试图接入无效内容或者尝试以不被允许的方式接入由于内存的保护等级(例如,尝试写入只读的内存)。 Exception Subtype 字段包含一个kern_return_t结构体用来描述错误和不正确接入的内存地址。

下面是一些调试坏内存接入导致崩溃的建议:

  • 假如objc_msgSend或者objc_release在崩溃线程回溯(Backtraces)的顶部附近,这个线程可能尝试给一个释放的对象发消息。你应该 profile 应用使用Zombies instrument来更好的理解这个崩溃发生的条件。
  • 假如gpus_ReturnNotPermittedKillClient在崩溃线程回溯(Backtraces)的顶部附近,线程被终结因为它尝试用OpenGL ES或者Metal执行渲染当程序处于后台时。查看QA1766: How to fix OpenGL ES application crashes when moving to the background
  • 打开 Address Sanitizer 运行你的应用。Address Sanitizer 添加了额外的说明在内容接入当你编译代码的时候。随着你应用的运行,Xcode 将⚠️你假如内存以一种可能导致崩溃的方式接入。

2.异常退出(Abnormal Exit)

程序异常退出,是最常见导致这类异常崩溃的原因是捕获到 Objective-C/C++ 异常和调用了 abort() 函数。

App Extensions将被终结发生这种类型的异常,假如他们初始化花费太多的时间(watchdog终结)。假如一个extension由于载入时间太长被终结,产生崩溃报告的Exception Subtype将是LAUNCH_HANG。因为extensions并没有一个 main 函数,任何花销在初始化的时间都发生在static constructors和呈现在你的extensions和依赖库的 +load 方法。你应该尽可能的延迟做这些工作。

3.追踪受限(Trace Trap)

和异常退出类似,这种异常的目的是给一个追加的调试器,让它有机会来打断在一个当它执行时候指定的点的进程。你可以使用 __builtin_trap() 函数在你的代码中来触发这个异常。假如没有调试器追加的话,线程将被终结并且产生一个崩溃报告。

低等级的库(例如libdispatch)将受限这个进程一旦遇到一个重大的错误。关于错误的额外信息可以在Additional Diagnostic Information章节中的崩溃报告找到,或者在设备的控制台。

假如在 runtime 遇到诸如下面的一个意外的条件, Swift 代码将终结出现这种类型的异常:

  • 非可选类型带有一个 nil
  • 错误的强制类型转换

查看Backtraces来决发生定异常条件的位置。额外的信息可能已经在设备的控制台打印出来了。你应该修改崩溃处的代码来优雅的处理 runtime 错误。例如,使用Optional Binding而不是强制解包一个可选变量。

4.非法指令(Illegal Instruction)

进程尝试执行一个非法或者未定义的指令。进程可能已经尝试跳进到一个无效的地址通过一个配置错误的函数指针。在 Intel 处理器中, ud2 操作码导致一个EXC_BAD_INSTRUCTION异常,但是它通常被用来困住进程达到调试的目的。 Swift 代码在 Intel 处理器中将以这种异常终结,假如在 runtime 位置条件发生。更多详情查看Trace Trap

5.被保护的资源遭到侵害(Guarded Resource Violation)

进程侵犯一个被保护的资源。系统库可能某个文件的描述器成 guarded ,在那以后,所有不正常的操作在这些描述器上都将触发一个EXC_GUARD异常(当它想操作在这些文件描述器上,系统可以使用特殊的 guarded 标记的私有 APIs)。这可以帮你向下快速追踪问题,例如关闭一个已经被系库打开的文件描述器。例如,假如一个 app 关闭文件秒杀器通过使用截图 SQLite 文件到一个 Core Data 存储, Core Data 将会在随后诡异的崩溃。guard exception将让这些问题尽早引起你的注意,这样也让他们变得更容易调试。

崩溃报告来自新版的 iOS 包含了人类可读的详细信息关于引起 EXC_GUARD 异常的操作在 Exception SubtypeException Message 字段中。在来自 macOS 或者老版本的 iOS 的崩溃报告中,这些信息被编码到第一个 Exception Code 就像一个分解成如下的位段:

  • [63:61] - Guard Type:被保护的资源类型。 0x2 代表一个文件描述器资源。
  • [60:32] - Flavor:侵害被处罚时的条件
    • 假如第一个 (1 << 0) 位被设置,进程尝试执行 close() 函数在一个受保护的文件描述器。
    • 假如第二个 (1 << 1) 位被设置,进程尝试执行 dup()dup2() ,或者 fcntl()F_DUPFD 或者 F_DUPFD_CLOEXEC 命令在一个受保护的文件描述器。
    • 假如第三个 (1 << 2) 位被设置,进程尝试通过一个 socket 发送给一个受保护的文件描述器。
    • 假如第三个 (1 << 3) 位被设置,进程尝试写入到一个受保护的文件描述器。
  • [31:0] - File Descriptor:进程尝试修改的受保护的文件描述器。

6.资源限制(Resource Limit)

进程超出了一个资源消耗的限制。这是一个来自操作系统通知,告诉进程正在使用的资源过多。精确的资源列在 Exception Subtype 字段中。假如 Exception Note 字段包含 NON-FATAL CONDITION ,进程不会被终结即使产生了一个崩溃报告。

  • 异常子类型 MEMORY 表明进程已经越过系统应用的内存限制。这可能是一个终结的先兆由于超额的使用内存。

  • 异常子类型 WAKEUPS 表明在进程中的线程每秒被唤醒太多次,这强制 CPU 非常频繁的唤醒消耗电池寿命。

    典型的,这个通过由线程与线程的通信产生(通常是使用 peformSelector:onThread:dispatch_async ),那样无意的发生了远远超出它正常应该的切换频率。因为通信的协调发生得非常频繁而出发此类的异常,这个通常和多个后台线程有着相似 Backtraces – 表明那些地方发生过通信。

7.其他异常类型(Other Exception Types)

一些崩溃报告可能含有一个未命名的 Exception Type ,将以一个 16 进制的值(例如,00000020)的形式打印。假如你的设备收到了一个这样的崩溃报告,直接查看 Exception Codes 字段寻找更多的信息。

  • 异常代码 0xbaaaaaad 表明记录是整个系统的 stackshot ,不是一个崩溃报告。为了获得一个 stackshot ,按 Home 键和任意音量键。这些记录经常被用户偶然创建,并不表明是一个错误。

  • 异常代码 0xbad22222 表明一个 VoIP 应用已经被 iOS 终结,因为它启动得太频繁。

  • 异常代码 0x8badf00d 表明应用已经被 iOS 终结因为发生 watchdog 超时。应用花费太长时间启动,终结,或者响应系统事件。通常导致这歌问题是做了在主线程执行了同步的网络请求。无论什么操作在 Thread 0 都需要移动到后台线程,或者异步处理,以免它阻塞主线程。

  • 异常代码 0xc00010ff 表明引用被操作系统终结为了响应一个发热事件。这个可能由于一个发生崩溃的特定的设备的问题或者环境被操作导致。为了使你的应用更高效运行的建议,查看WWDC session iOS Performance and Power Optimization with Instruments

  • 异常代码 0xdead10cc 表明应用被 iOS 终结,由于当在后台运行时它持有了一个系统的资源(像通信录数据库)。

  • 异常代码 0xdeadfa11 表明应用被用户强制退出。强制退出发生在当用户第一次按下开关机按钮直到”滑动来关机”出现,然后在按下 Home 键。这是合理的假如用户这样做了,因为应用已经变得不可响应,但是这并不能保证 - 强制退出任何正在运行的任务。

    注意:终结一个挂起的 app 通过从多任务关系面板中移除并不会产生一个崩溃报告。一旦一个 app 被挂起,iOS它有资格在任何时候终结它,所有没有崩溃报告产生。

参考资料

前言

一直在观察各种 App 的 LoadingView ,比较有代表性的是 MBProgressVHUDSVProgressHUD ,这两个使用得非常广泛,大到 QQ,支付宝,小到各种不知道名的 App,长时间的迭代让它们的逻辑非常完善,同时也导致了有些累赘,如果把它们当一个产品来分析,可以看出,它们在不断地增加需求和使用场景,有没有一个非常简洁的 HUD 没有我们不需要的那些多余的逻辑只是负责显示指示和隐藏呢?

好像没有,所有今天动手准备自己封装一个 LoadingView ,灵感来自 Airbnb,用过 Airbnb 的同学都知道,它的 LoadingView 很有风格,Airbnb 是几张图片循环翻转切换,当然我不准备复制他们的 idea,我准备做一个循环左右上下切换的 LoadingView

废话不多说,先来看一下,最终效果的原型图

RFLoadingView

思路

分析一下思路:

  • 四条虚线交叉形成的区域就是我们能够看到的图片
  • 首先准备两张图片,位置 中+上
  • 开始第一段动画,向下切换,位置 变成 中+下
  • 第一段动画结束,将下面的图片移动到右边,准备开始第二段动画
  • 第二动画跟第一段类似,只是方向是从右向左,动画结束后 位置变成 中+左
  • 将左边的图片移动到上方位置,完成一个循环

为了使动画更流畅不至于生硬,我们使用 iOS7 推出的带弹簧效果的 API

1
2
3
 
+ (void)animateWithDuration:(NSTimeInterval)duration delay:(NSTimeInterval)delay usingSpringWithDamping:(CGFloat)dampingRatio initialSpringVelocity:(CGFloat)velocity options:(UIViewAnimationOptions)options animations:(void (^)(void))animations completion:(void (^ __nullable)(BOOL finished))completion NS_AVAILABLE_IOS(7_0);

API 接口

在 API 接口设计上,我希望尽量简单实用,封装了两个方法,一个用来显示,一个用来隐藏,可以指定显示到某个 view,以及指定显示和隐藏时是否使用动画。

1
2
3
4
 
+ (void)showViewAddedTo:(UIView *)view animated:(BOOL)animated;
+ (void)hideViewForView:(UIView *)view animated:(BOOL)animated;

API 实现

根据刚才的分析,核心的动画实现已经有了思路,现在就是怎么设计内部代码实现,为了方便显示蒙版阻止加载的时候用户交互,我把 view 的背景颜色设置了一个淡灰色,view 的中间有三个子 view,一个是位于中间的容器 centralView ,负责显示我们所看到的区域,方便实现圆角和动画效果,里面加入两个子 view, firstViewsecondView 用于显示动画切换的图片。

内部接口大概这样

1
2
3
4
5

@property (nonatomic, strong) UIView *centralView;
@property (nonatomic, strong) UIImageView *firstView;
@property (nonatomic, strong) UIImageView *secondView;

为了防止多次添加 LoadingView ,在每次添加前,我们会查找该 view 是否存在,如果不存在,创建一个新的对象,如果存在直接跳过添加操作。反向遍历,快速查找。

1
2
3
4
5
6
7
8
9
10
11
12
13

+ (RFLoadingView *)loadingViewForView:(UIView *)view
{
NSEnumerator *subviewsEnum = [view.subviews reverseObjectEnumerator];
for (UIView *subview in subviewsEnum) {
if ([subview isKindOfClass:self]) {
return (RFLoadingView *)subview;
}
}

return nil;
}

动画核心实现,两端动画,封装成两个方法

  • - (void)animatedImageFromTopToBottom * - (void)animatedImageFromTopToBottom 具体实现如下
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

- (void)animatedImageFromTopToBottom
{
[UIView animateWithDuration:0.3 delay:0.5 usingSpringWithDamping:0.7 initialSpringVelocity:0 options:UIViewAnimationOptionCurveEaseInOut animations:^{
_firstView.centerY += kCenterViewSize;
_secondView.centerY += kCenterViewSize;
} completion:^(BOOL finished) {
_firstView.centerX += kCenterViewSize;
_firstView.centerY -= kCenterViewSize;

[self changeFirstImage];
[self animatedImageFromRightToLeft];
}];
}

- (void)animatedImageFromRightToLeft
{

[UIView animateWithDuration:0.3 delay:0.5 usingSpringWithDamping:0.7 initialSpringVelocity:0 options:UIViewAnimationOptionCurveEaseInOut animations:^{
_firstView.centerX -= kCenterViewSize;
_secondView.centerX -= kCenterViewSize;
} completion:^(BOOL finished) {
_secondView.centerX += kCenterViewSize;
_secondView.centerY -= kCenterViewSize;

if (self.alpha && self) {
[self changeSecondImage];
[self animatedImageFromTopToBottom];
}

}];
}

需要非常注意的是,一定要写上终止动画的条件,不然会无限循环,影响性能

show 和 hide

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
- (void)showAnimated:(BOOL)animated
{
RFMainThreadAssert();

self.alpha = 1;

[UIView animateWithDuration:animated ? 0.3 : 0 animations:^{
_centralView.alpha = 1;
} completion:^(BOOL finished) {
[self animatedImageFromTopToBottom];
}];
}

- (void)hideAnimated:(BOOL)animated
{
RFMainThreadAssert();

[UIView animateWithDuration:animated ? 0.3 : 0 animations:^{
self.alpha = 0;
} completion:^(BOOL finished) {
[self removeFromSuperview];
}];
}

最终效果