beginTime 的妙用
创建一个动画,添加到不同的图层上,可以实现复用,通过调节 beginTime
可以调节动画开始执行的时间。
例如,我们创建一个水平位移动画,添加到不同的图层对象上,并且让他们轮流执行。
翻译自 NSHipster 的UIActivityViewController
播放音频可以通过:
1 | MPMusicPlayerController *iPod = [MPMusicPlayerController systemMusicPlayer]; |
播放视频可以通过:
1 | float outputVolume = [[AVAudioSession sharedInstance] outputVolume]; |
推荐下面的方法,上面的在某些版本可能有问题,下面的方法兼容iOS6 及以上。
通过设置音频会话的 category 实现:
1 | NSError *setCategoryError = nil; |
这样 App 就不会随着手机静音键打开而静音,可在手机静音下播放声音😁
监听音频改变私有通知:
1 | [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(volumeChanged:) name:@"AVSystemController_SystemVolumeDidChangeNotification" object:nil]; |
实现通过回调:
1 | - (void)volumeChanged:(NSNotification *)notification |
使用 MPVolumeView
类,便利它的子 views 找到类为 MPVolumeSlider
的滑竿。
1 | MPVolumeView *volumeView = [[MPVolumeView alloc] init]; |
然后再通过设置 volumeViewSlider
的 value
即可。
1 | _volumeViewSlider.value = someVolume; |
参考Sound Switch - Sharkfood的实现。
使用很简单,判断是否为静音模式:
1 | if ([SharkfoodMuteSwitchDetector shared].isMute) { |
动态监听,通过 block 回调:
1 | [SharkfoodMuteSwitchDetector shared].silentNotify = ^(BOOL silent){ |
监听 AVAudioSessionRouteChangeNotification
通知:
1 | [[NSNotificationCenter defaultCenter] addObserver:self |
实现回调:
1 | - (void)audioRouteChangeListenerCallback:(NSNotification*)notification |
UIImageView
应该是iOS中使用最频换的控件,就如日常吃饭一样,天天都在重复,有时或许应该反思一下,怎么使用这个控件,达到低能耗,最佳用户体验。
针对单张图片来说,常见的处理是在图片准备显示时增加一个淡出动画,能使图片显示闲的很平滑。
多张图片也一样,在第一张图片的基础上淡出原来的图片,淡入新的图片。也可以说是溶解效果。
很多人喜欢对图片的 alpha
做淡出动画,使 alpha
从 0 到 1 动画改变。这种动画有一点不好的是,在动画结束后,图片会明显的出现一闪,这样使动画看起来有点突兀。比较好的做法时,在将要显示时给图片做一个转场动画。
下面是其中一种简单的实现:
1 | @implementation UIImageView (RFWebImage) |
思路:在 ImageView 将要显示是使用转场动画函数来实现淡出动画效果,体验应该是是各种动画中最好的了,而且使用起来很简单。
在淡出显示的动画基础上,我们引出今天的主角,动画切换 Image。
思路:单张图片淡出我们已经实现,现在做的就是在切换一张新的图片时同时再加入淡出或者说溶解效果即可。
比较常见的有下面 3 种实现:
CATransition
类实现UIView
动画转场 API 实现CABasicAnimation
类实现 CATransition
类是iOS中很好用的控制转场动画的类,通过简单的配置可以实现常见而炫酷的动画效果,变换类型通过 type
字段控制, subtype
可以很细化控制动画的方向(比如动画开始的上下左右方向)。 CATransition
继承至 CAAnimation
可以对动画设置动画曲线(timingFunction),可以通过代理获取动画状态(是已经开始,还是已经停止,已经是否完成)。
type
支持四种类型:
下面是样板代码:
1 | - (void)animatedSwichImageMethodOne { |
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); |
通过上面的函数实现,其实是对第一种的高级封装。通过设置 options
为 UIViewAnimationOptionTransitionCrossDissolve
即可。
下面是样板代码:
1 | - (void)animatedSwichImageMethodTwo { |
CABasicAnimation
是核心动画一个重要的类,继承至 CAPropertyAnimation
,可以对所有的可动画属性做动画,可以通过 fromValue
, toValue
, byValue
字段控制动画的进度。
在这里我们是对 CALayer
的 contents
属性做动画,在改变图片时,创建一个 CABasicAnimation
对象添加到 ImageView 的图层上即可。
下面是样板代码:
1 | - (void)animatedSwichImageMethodThree { |
更多内容请下载Demo查看(🤔Bonus: Flip 效果🤔)
在 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
也没有对应的桥接类型 CFBundle
, NSDateFormatter
同样没有对应的桥接类型 CFDateFormatter
。
文章末尾表 1 提供了一份支持无缝桥接的数据类型的列表。
注意:假如你使用一个自定义回调在一个
Core Foundation
框架的集合中,包含一个NULL
回调,当使用Objective-C
的方式接入它,它的内存管理方式是未定义的。
通过无缝桥接技术,在一个你以 NSLocale *
做为一个参数的方法的例子中,你能传递一个 CFLocaleRef
结构体,并且当你看到有一个 CFLocaleRef
参数的函数中,你能够传递一个 NSLocale
实例对象。当然你也必须提供给编译器相关的一些其它信息:第一,你必须转换一种类型成其它;第二,你可能必须指明对象的语义生命周期。
编译器理解 Objective-C
的方法并且返回 Core Foundation
数据类型,下面是 Cocoa
命名转换的历史(查看Advanced Memory Management Programming Guide)。例如,编译器知道,在 iOS
中,通过 UIColor
的 CGColor
方法返回的 CGColor
并不应该被持有。你必须使用恰当的类型转换,像下面例子演示的那样:
1 | NSMutableArray *colors = [NSMutableArray arrayWithObject:(id)[[UIColor darkGrayColor] CGColor]]; |
编译器并不会自动管理 Core Foundation
对象的生命周期。你必须告诉编译器对象的语义所属关系通过使用一种转换(定义在objc/runtime.h)或者 Core Foundation
风格的宏(定义在 NSObject.h):
__bridge
关键字表示转换指针在 Objective-C
和 Core Foundation
之间而不会转换所属关系。__bridge_retained
关键字或者 CFBridgingRetain
表示转换指针在 Objective-C
和 Core Foundation
之间并且把所属权交给你。你负责调用 CFRelease
或者相关的函数来交出对象的所属权。__bridge_transfer
关键字或者 CFBridgingRelease
表示转换一个非 Objective-C
的指针到 Objective-C
并且转换所属权给ARC。ARC负责交出对象的所属权。下面是一些例子:
1 | NSLocale *gbNSLocale = [[NSLocale alloc] initWithLocaleIdentifier:@"en_GB"]; |
下面的例子显示了使用口述的 Core Foundation
内存管理规则来管理 Core Foundation
内存:
1 | - (void)drawRect:(CGRect)rect { |
下面的表格提供了一个在 Core Foundation
和 Foundation
中可以交替转换数据类型列表。对每一对桥接类型,表也列举出了这些无缝桥接类型在 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 |
随著 block
在iOS4.0和OS X 10.6的引入,给事件传递一种新的方式实现,在开发中用得最多的场景莫过于事件回调。使用 block
相对与 delegate
的优势在于,业务集中,可读性强,代码内联,不像代理需要实现很多函数,在适当的场景选择这种方式实现事件传递或者传参效果非常好,现在很多开源项目都实现了两种方法的事件回调。
block
用起来虽然很爽,但也有它的不足,存在循环引用,轻者内存泄露,甚至导致App崩溃,不易调试追溯,因此使用它使一定要小心。鉴于实践中的踩过各种坑,总结下来,方便自己和他人以后查阅,这就是block备忘录写作的初衷。
block
实际上是指向结构体的指针,编译时, block
的内部代码生产对应的函数。
具体结构如下:
block
的代码是内联的,效率高于函数调用block
对于外部变量默认是只读属性block
被 Objective-C
看成是对象处理作为 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 | typedef void(^SDWebImageCompletionBlock)(UIImage *image, NSError *error, SDImageCacheType cacheType, NSURL *imageURL); |
跟 C 函数类似使用 ()
,括号里面还可以带一个或者多个参数
1 | // block 声明 |
默认情况下, block
是在栈内存中,它不会对所引用的对象进行任何操作;如果对 block
进行一次 copy
操作, block
就会在堆内存中,并且它会它所有的引用的对象做一次 retain
操作
__block
修饰的外部变量引用,block 是复制其引用地址来实现访问的__block
关键词ARC
如果对象使用 `__unsafe_unretained` 或 `__weak` 修饰,就不会对其做 `retain` 操作
MRC
如果对象使用了 `__block` 修饰, 就不会对其做 `retain` 操作
为了防止 block
中的循环引用,可以用 __weak
关键词把相应的对象声明为弱引用,在 block
快内部需要多次访问,防止该对象被释放,可以用 __strong
关键词将声明为强引用:
1 | __weak __typeof__(self) weakSelf = self; |
此文是基于这些年工作中项目里面常见崩溃的一些总结,整理出来方便查阅,希望对大家都有所帮助。
NSAttributedString
相关tableView
数据示例代码:
1 | - (void)testArrayOutOfBounds |
异常现象:
Terminating app due to uncaught exception ‘NSRangeException’, reason: ‘** -[__NSArrayI objectAtIndex:]: index 3 beyond bounds [0 .. 2]’*
预防方案:
在数组中取值时需要先进组下标索引边界检查,如果没有越界方可取值。
示例代码:
1 | - (void)testDicSetNilValueCrash |
异常现象:
Terminating app due to uncaught exception ‘NSInvalidArgumentException’, reason: ‘** -[__NSPlaceholderDictionary initWithObjects:forKeys:count:]: attempt to insert nil object from objects[0]’*
预防方案:
在我们使用字面量快速创建一个字典的时候需要特别小心,因为很可能字典的键和值不能保证同时不为空。有潜在崩溃的风险,这种崩溃非常容易出现,需要特别小心,但是当你留心的话也非常好避免,就是设置字典的键或者值的时候判断是否非空,可变字典设置某个键的值是可以为空,相当于删除字典中的某个键值。为了使 App 保持健壮推荐使用 KVO
或者字面量的方式来设置字典的值
1 | - (void)testMutableDicSetNilValueCrash |
示例代码:
1 | - (void)testAttributedStringInitCrash |
异常现象:
Terminating app due to uncaught exception ‘NSInvalidArgumentException’, reason: ‘NSConcreteMutableAttributedString initWithString:: nil value’
预防方案:
在构造 NSMutableAttributedString
或者 NSAttributedString
需要留意,设置的属性的值是否有可能存在 nil
的情况。这个很容易被人忽视,值得注意。
示例代码:
1 | - (void)testPresentNilControllerCrash |
异常现象:
present 一个空的控制器导致 App crash
预防方案:
present 一个新控制器时,判断是否存在,存在才执行,否则直接返回
1 | - (void)testPresentNilControllerCrashFixed |
示例代码:
1 | - (void)testUnrecogernizedSelectorCash |
异常现象:
Terminating app due to uncaught exception ‘NSInvalidArgumentException’, reason: ‘-[ViewController testSel]: unrecognized selector sent to instance 0x7ffd41609d10’
预防方案:
此类崩溃经常出现,特别是当服务器数据放回异常时,比如本来应该返回一个 NSString
类型字符串,结果返回 NULL
,当你调用字符串的 length
方式时,导致 App 崩溃。预防方法,重要的地方对类型进行判断再调用该类的相关方法,或者写一个分类统一处理此类逻辑。
示例代码:
1 | - (void)testTableViewUpdateCrash |
异常现象:
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
的后的数据保持同步。
异常现象:
Fatal Exception: NSInvalidArgumentException
Pushing the same view controller instance more than once is not supported (<PPSelectPayMethodViewControllerIOS7: 0x10d7e7f10>)
参考链接:
以上就是工作中常见的异常崩溃以及处理方案,下面的异常分类内容来自 Apple
的官方文档,有兴趣的可以查阅。☕️
[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. 其他异常类型当程序试图接入无效内容或者尝试以不被允许的方式接入由于内存的保护等级(例如,尝试写入只读的内存)。 Exception Subtype
字段包含一个kern_return_t结构体用来描述错误和不正确接入的内存地址。
下面是一些调试坏内存接入导致崩溃的建议:
Address Sanitizer
运行你的应用。Address Sanitizer 添加了额外的说明在内容接入当你编译代码的时候。随着你应用的运行,Xcode 将⚠️你假如内存以一种可能导致崩溃的方式接入。程序异常退出,是最常见导致这类异常崩溃的原因是捕获到 Objective-C/C++
异常和调用了 abort()
函数。
App Extensions将被终结发生这种类型的异常,假如他们初始化花费太多的时间(watchdog终结)。假如一个extension由于载入时间太长被终结,产生崩溃报告的Exception Subtype将是LAUNCH_HANG。因为extensions并没有一个 main
函数,任何花销在初始化的时间都发生在static constructors和呈现在你的extensions和依赖库的 +load
方法。你应该尽可能的延迟做这些工作。
和异常退出类似,这种异常的目的是给一个追加的调试器,让它有机会来打断在一个当它执行时候指定的点的进程。你可以使用 __builtin_trap()
函数在你的代码中来触发这个异常。假如没有调试器追加的话,线程将被终结并且产生一个崩溃报告。
低等级的库(例如libdispatch)将受限这个进程一旦遇到一个重大的错误。关于错误的额外信息可以在Additional Diagnostic Information章节中的崩溃报告找到,或者在设备的控制台。
假如在 runtime
遇到诸如下面的一个意外的条件, Swift
代码将终结出现这种类型的异常:
nil
值查看Backtraces来决发生定异常条件的位置。额外的信息可能已经在设备的控制台打印出来了。你应该修改崩溃处的代码来优雅的处理 runtime
错误。例如,使用Optional Binding而不是强制解包一个可选变量。
进程尝试执行一个非法或者未定义的指令。进程可能已经尝试跳进到一个无效的地址通过一个配置错误的函数指针。在 Intel
处理器中, ud2
操作码导致一个EXC_BAD_INSTRUCTION异常,但是它通常被用来困住进程达到调试的目的。 Swift
代码在 Intel
处理器中将以这种异常终结,假如在 runtime
位置条件发生。更多详情查看Trace Trap。
进程侵犯一个被保护的资源。系统库可能某个文件的描述器成 guarded
,在那以后,所有不正常的操作在这些描述器上都将触发一个EXC_GUARD异常(当它想操作在这些文件描述器上,系统可以使用特殊的 guarded
标记的私有 APIs)。这可以帮你向下快速追踪问题,例如关闭一个已经被系库打开的文件描述器。例如,假如一个 app 关闭文件秒杀器通过使用截图 SQLite
文件到一个 Core Data
存储, Core Data
将会在随后诡异的崩溃。guard exception将让这些问题尽早引起你的注意,这样也让他们变得更容易调试。
崩溃报告来自新版的 iOS
包含了人类可读的详细信息关于引起 EXC_GUARD
异常的操作在 Exception Subtype
和 Exception Message
字段中。在来自 macOS
或者老版本的 iOS
的崩溃报告中,这些信息被编码到第一个 Exception Code
就像一个分解成如下的位段:
0x2
代表一个文件描述器资源。(1 << 0)
位被设置,进程尝试执行 close()
函数在一个受保护的文件描述器。(1 << 1)
位被设置,进程尝试执行 dup()
, dup2()
,或者 fcntl()
带 F_DUPFD
或者 F_DUPFD_CLOEXEC
命令在一个受保护的文件描述器。(1 << 2)
位被设置,进程尝试通过一个 socket
发送给一个受保护的文件描述器。(1 << 3)
位被设置,进程尝试写入到一个受保护的文件描述器。进程超出了一个资源消耗的限制。这是一个来自操作系统通知,告诉进程正在使用的资源过多。精确的资源列在 Exception Subtype
字段中。假如 Exception Note
字段包含 NON-FATAL CONDITION
,进程不会被终结即使产生了一个崩溃报告。
异常子类型 MEMORY
表明进程已经越过系统应用的内存限制。这可能是一个终结的先兆由于超额的使用内存。
异常子类型 WAKEUPS
表明在进程中的线程每秒被唤醒太多次,这强制 CPU
非常频繁的唤醒消耗电池寿命。
典型的,这个通过由线程与线程的通信产生(通常是使用 peformSelector:onThread:
或 dispatch_async
),那样无意的发生了远远超出它正常应该的切换频率。因为通信的协调发生得非常频繁而出发此类的异常,这个通常和多个后台线程有着相似 Backtraces
– 表明那些地方发生过通信。
一些崩溃报告可能含有一个未命名的 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
,比较有代表性的是 MBProgressVHUD
, SVProgressHUD
,这两个使用得非常广泛,大到 QQ,支付宝,小到各种不知道名的 App,长时间的迭代让它们的逻辑非常完善,同时也导致了有些累赘,如果把它们当一个产品来分析,可以看出,它们在不断地增加需求和使用场景,有没有一个非常简洁的 HUD 没有我们不需要的那些多余的逻辑只是负责显示指示和隐藏呢?
好像没有,所有今天动手准备自己封装一个 LoadingView
,灵感来自 Airbnb,用过 Airbnb 的同学都知道,它的 LoadingView
很有风格,Airbnb 是几张图片循环翻转切换,当然我不准备复制他们的 idea,我准备做一个循环左右上下切换的 LoadingView
。
废话不多说,先来看一下,最终效果的原型图
分析一下思路:
为了使动画更流畅不至于生硬,我们使用 iOS7 推出的带弹簧效果的 API
1 |
|
在 API 接口设计上,我希望尽量简单实用,封装了两个方法,一个用来显示,一个用来隐藏,可以指定显示到某个 view,以及指定显示和隐藏时是否使用动画。
1 |
|
根据刚才的分析,核心的动画实现已经有了思路,现在就是怎么设计内部代码实现,为了方便显示蒙版阻止加载的时候用户交互,我把 view 的背景颜色设置了一个淡灰色,view 的中间有三个子 view,一个是位于中间的容器 centralView
,负责显示我们所看到的区域,方便实现圆角和动画效果,里面加入两个子 view, firstView
和 secondView
用于显示动画切换的图片。
内部接口大概这样
1 |
|
为了防止多次添加 LoadingView
,在每次添加前,我们会查找该 view 是否存在,如果不存在,创建一个新的对象,如果存在直接跳过添加操作。反向遍历,快速查找。
1 |
|
动画核心实现,两端动画,封装成两个方法
- (void)animatedImageFromTopToBottom
* - (void)animatedImageFromTopToBottom
具体实现如下1 |
|
需要非常注意的是,一定要写上终止动画的条件,不然会无限循环,影响性能
1 | - (void)showAnimated:(BOOL)animated |