0%

使用场景

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

解决的问题

以一种直观的方式,为视图创建约束

怎样解决

核心方法:

1
2
3
4
+ (NSArray<__kindof NSLayoutConstraint *> *)constraintsWithVisualFormat:(NSString *)format
options:(NSLayoutFormatOptions)opts
metrics:(nullable NSDictionary<NSString *,id> *)metrics
views:(NSDictionary<NSString *, id> *)views;

Example:

1
2
3
4
[NSLayoutConstraint constraintsWithVisualFormat:@"|-[button1]-[button2]-[textField(>=20)]-|"
options:0
metrics:metrics
views:views]

参数:

  • format:指定约束的格式。更多信息,在Auto Layout Guide查看Visual Format Language
  • opts:描述在视觉格式化字符串中的布局属性和方向
  • metrics:将出现在视觉格式化字符串的常量字典。字典的Keys必须是在出现在视觉格式化字符串的字符串类型,对应的values必须是NSNumber对象。
  • views:出现的视觉格式化字符串的字典view,所有的Keys必须是使用在视觉格式化字符串的字符串类型,对应的values必须是view对象。

返回值:

一个约束组合的数组,像视觉格式化字符串描述的一样表述了在提供的视图和它们的父视图之间的关系。所有的约束以在视觉格式化字符串被指定的约束顺序一致。

效率和可维护性

需要对VFL理解比较全面和深入,有一定的学习成本,维护起来比较困难,但是如果理解的比较深刻,对于简单的布局效率非常高,比直接创建约束要直观。

最佳实践

  • 每条约束格式字符串分水平(H,可省略)和垂直方向(V)。
  • | 代表父视图。
  • - 代表标准间距,两个子视图直接的值是8,与父视图之间的值是16。
  • [view] 每个视图必须用[]包裹起来,否则语法错误。
  • [view(>=44)] 可以为每个视图设置一些属性或者关系,写一个紧跟view后的括号集合,支持宽高,优先级,和其它视图之间的关系。
  • [view@20] 可以设置视图的约束的优先级,以@开头,取值范围(0 1000]。
  • [view1]-20-[view2] 可以指定view之间的水平或者垂直间距,写在一对 - 即可。
  • [view1][view2] 如果 - 省略则他们之间的距离为0。
  • [flexibleButton(>=70,<=100)] 多个条件之间用,连接并且之间不能有空格。

其它方案

  1. 使用NSLayoutConstraint的类方法创建(iOS96.0及以上可用)
1
2
3
4
5
6
7
+ (instancetype)constraintWithItem:(id)view1
attribute:(NSLayoutAttribute)attr1
relatedBy:(NSLayoutRelation)relation
toItem:(nullable id)view2
attribute:(NSLayoutAttribute)attr2
multiplier:(CGFloat)multiplier
constant:(CGFloat)c;
  1. 使用NSLayoutAnchor的工厂方法创建(iOS9.0及以上可用)
1
2
3
4
5
6
7
- (NSLayoutConstraint *)constraintEqualToAnchor:(NSLayoutAnchor<AnchorType> *)anchor;
- (NSLayoutConstraint *)constraintGreaterThanOrEqualToAnchor:(NSLayoutAnchor<AnchorType> *)anchor;
- (NSLayoutConstraint *)constraintLessThanOrEqualToAnchor:(NSLayoutAnchor<AnchorType> *)anchor;

- (NSLayoutConstraint *)constraintEqualToAnchor:(NSLayoutAnchor<AnchorType> *)anchor constant:(CGFloat)c;
- (NSLayoutConstraint *)constraintGreaterThanOrEqualToAnchor:(NSLayoutAnchor<AnchorType> *)anchor constant:(CGFloat)c;
- (NSLayoutConstraint *)constraintLessThanOrEqualToAnchor:(NSLayoutAnchor<AnchorType> *)anchor constant:(CGFloat)c;

使用注意事项

  • 使用时,必须把view的translatesAutoresizingMaskIntoConstraints属性设置为NO,否则约束可能更预期不一致。这是一个历史遗留的问题,由于在AutoLayout诞生以前一直使用Autoresizing来控制布局.

  • 重写updateConstraints()方法,在里面计算约束,然后调用setNeedsUpdateConstraints()触发更新约束。

  • 系统计算布局顺序:

    • updateConstraints()
    • layoutSubviews()
    • drawRect(_:)

系统计算布局顺序参考

LayoutCycle

具体使用示例,查看下面的Demo,样品工程


Demo地址:VFLDemo

参考链接

翻译自NSHipster的UIActivity​View​Controller

一直非常好奇代码和数据之间的联系。

某些编程语言,例如LispIoMathematica他们是同型性(代码即数据),那就意味着它们的代码是原始数据的呈现形式,它们本身在代码中也能够被维护。然而大多数其它语言,包括Objective-C,然而,在这两者之间有严格的边界,避免eval()和其它有动态引导加载的潜在危险方法。

当数据出现太大和呈现一些东西过于笨重除了使用二进制流的问题时代码和数据之间的不安被推向到了一个全新的高度。自从第一个操作系统出现开始,怎样编码,解码,解释二进制代表的图片,文档,和媒体一直是一个问题。

在OS X上的核心服务框架和iOS上提供标识功能和通过文件拓展和MIMIE type–根据通用数据标示,给数据类型的分类核心服务框架。UTIs提供了一种可拓展,阶梯分层系统,这给开发者在处理大量不同的文件类型提供了巨大的灵活性。例如,一个Ruby的源文件(.rb)被归类为:Ruby Source Code > Source Code > Text > Content > Data;一个QuickTime电影文件(.mov)被归类为 Video > Movie > Audiovisual Content > Content > Data。

UTIs表现得非常好在桌面的抽象文件系统中。然而,在一个移动范例中这就崩溃得很快,因为文件和目录是对用户隐藏的。还有,随着云服务和社交媒体扮演着越来越重要的角色在远程实体通过本地文件。因此,UTIs和URLs之间很紧张。

我们非常清楚我们需要一些别的东西。UIActivityViewController能否成为我们正在急切寻找的解决方案呢?


UIActivityViewController,在iOS6中被引入,提供了一套分享和在应用内执行数据操作的统一服务界面。

考虑到可操作的数据集合,一个UIActivityViewController实例通过如下方式创建:

1
2
3
4
5
6
7
8
9
10
NSString *string = ...;
NSURL *URL = ...;

UIActivityViewController *activityViewController = [[UIActivityViewController alloc] initWithActivityItems:@[string, URL]
applicationActivities:nil];
[navigationController presentViewController:activityViewController
animated:YES
completion:^{
// ...
}];

这将在屏幕底部呈现如下界面:

activity sharing img

默认情况下,UIActivityViewController将会显示所有支持的可用服务类型,但是某些活动类型也可以排除:

1
activityViewController.excludedActivityTypes = @[UIActivityTypePostToFacebook];

活动类型被划分“动作”(action)和“分享”(share)类型:

UIActivityCategoryAction

  • UIActivityTypePrint
  • UIActivityTypeCopyToPasteboard
  • UIActivityTypeAssignToContact
  • UIActivityTypeSaveToCameraRoll
  • UIActivityTypeAddToReadingList
  • UIActivityTypeAirDrop

UIActivityCategoryShare

  • UIActivityTypeMessage
  • UIActivityTypeMail
  • UIActivityTypePostToFacebook
  • UIActivityTypePostToTwitter
  • UIActivityTypePostToFlickr
  • UIActivityTypePostToVimeo
  • UIActivityTypePostToTencentWeibo
  • UIActivityTypePostToWeibo

每种活动类型,支持一系列不同的数据类型。例如,一个Tweet可能由一个NSString并附带一个image和或者URL

不同活动类型支持的数据类型

Activity Type String Attributed String URL Data Image Asset Other
Post To Facebook
Post To Twitter
Post To Weibo
Message ✓* ✓* sms:// NSURL
Mail ✓+ ✓+ ✓+
Print ✓+ ✓+ UIPrintPageRenderer, UIPrintFormatter, & UIPrintInfo
Copy To Pasteboard UIColor, NSDictionary
Assign To Contact
Save To Camera Roll
Add To Reading List
Post To Flickr
Post To Vimeo
Post To Tencent Weibo
AirDrop

UIActivityItemSource & UIActivityItemProvider

当必要时和一个粘贴板项目能用来提供的数据类似,为了避免过度的内测开销或处理时间,活动类型可以是一种自定义类型。

任何遵循<UIActivityItemSource>协议的对象,包括内构UIActivityItemProvider类,可以用来动态提供不同类型的数据根据活动的类型。

UIActivityItemSource

获取数据项目:

  • activityViewControllerPlaceholderItem:
  • activityViewController:itemForActivityType:

提供数据项目信息:

  • activityViewController:subjectForActivityType:
  • activityViewController:dataTypeIdentifierForActivityType:
  • activityViewController:thumbnailImageForActivityType:suggestedSize:

下面是一个例子,根据是否分享到FaceBook或者Twitter来展示自定义一条消息是怎样使用的:

1
2
3
4
5
6
7
8
9
10
11
- (id)activityViewController:(UIActivityViewController *)activityViewController
itemForActivityType:(NSString *)activityType
{
if ([activityType isEqualToString:UIActivityTypePostToFacebook]) {
return NSLocalizedString(@"Like this!");
} else if ([activityType isEqualToString:UIActivityTypePostToTwitter]) {
return NSLocalizedString(@"Retweet this!");
} else {
return nil;
}
}

创建一个自定义UIActivity

除了上述的系统提供的活动,你也可以创建你自己的活动。

例如,让我们创建一个自定义活动类型,它能使一个URL中的图片添加上胡须通过使用mustache.me

Before:

Before

After:

After

首先,我们定义一个反向域名解析的标识符给活动类型:

1
static NSString * const HIPMustachifyActivityType = @"com.nshipster.activity.Mustachify";

然后指定它的分类为UIActivityCategoryAction并且提供一个本地化标题和iOS版本相应的图片:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#pragma mark - UIActivity

+ (UIActivityCategory)activityCategory {
return UIActivityCategoryAction;
}

- (NSString *)activityType {
return HIPMustachifyActivityType;
}

- (NSString *)activityTitle {
return NSLocalizedString(@"Mustachify", nil);
}

- (UIImage *)activityImage {
if (NSFoundationVersionNumber > NSFoundationVersionNumber_iOS_6_1) {
return [UIImage imageNamed:@"MustachifyUIActivity7"];
} else {
return [UIImage imageNamed:@"MustachifyUIActivity"];
}
}

下一步,我们创建一个辅助函数,HIPMatchingURLsInActivityItems,通过它返回一个支持类型的图片URL的数组。

1
2
3
4
5
6
7
8
9
10
11
12
static NSArray * HIPMatchingURLsInActivityItems(NSArray *activityItems) {
return [activityItems filteredArrayUsingPredicate:[NSPredicate predicateWithBlock:
^BOOL(id item, __unused NSDictionary *bindings) {
if ([item isKindOfClass:[NSURL class]] &&
![(NSURL *)item isFileURL]) {
return [[(NSURL *)item pathExtension] caseInsensitiveCompare:@"jpg"] == NSOrderedSame ||
[[(NSURL *)item pathExtension] caseInsensitiveCompare:@"png"] == NSOrderedSame;
}

return NO;
}]];
}

这个函数让后被用在-canPerformWithActivityItems:prepareWithActivityItems:来获取小胡子的第一张PNG或JPEG图片的URL,假如有的话。

1
2
3
4
5
6
7
8
9
- (BOOL)canPerformWithActivityItems:(NSArray *)activityItems {
return [HIPMatchingURLsInActivityItems(activityItems) count] > 0;
}

- (void)prepareWithActivityItems:(NSArray *)activityItems {
static NSString * const HIPMustachifyMeURLFormatString = @"http://mustachify.me/%d?src=%@";

self.imageURL = [NSURL URLWithString:[NSString stringWithFormat:HIPMustachifyMeURLFormatString, self.mustacheType, [HIPMatchingURLsInActivityItems(activityItems) firstObject]]];
}

我们的网站服务提供了各种胡须选项,它被定义在一个枚举中:

1
2
3
4
5
6
7
8
typedef NS_ENUM(NSInteger, HIPMustacheType) {
HIPMustacheTypeEnglish,
HIPMustacheTypeHorseshoe,
HIPMustacheTypeImperial,
HIPMustacheTypeChevron,
HIPMustacheTypeNatural,
HIPMustacheTypeHandlebar,
};

最终,我们提供了一个UIViewController来显示图片。在这个例子中,一个简单的UIWebView控制器就够用了。

1
2
3
4
5
6
7
8
9
10
11
12
@interface HIPMustachifyWebViewController : UIViewController <UIWebViewDelegate>
@property (readonly, nonatomic, strong) UIWebView *webView;
@end

- (UIViewController *)activityViewController {
HIPMustachifyWebViewController *webViewController = [[HIPMustachifyWebViewController alloc] init];

NSURLRequest *request = [NSURLRequest requestWithURL:self.imageURL];
[webViewController.webView loadRequest:request];

return webViewController;
}

为了使用我们加了新胡须的活动,我们简单把它传递到UIActivityViewController构造器中:

1
2
3
4
HIPMustachifyActivity *mustacheActivity = [[HIPMustachifyActivity alloc] init];
UIActivityViewController *activityViewController =
[[UIActivityViewController alloc] initWithActivityItems:@[imageURL]
applicationActivities:@[mustacheActivity]];

手动执行动作

现在是一个提醒的好时机,当UIActivityViewController允许用户执行他们选择的动作,分享也能通过手动执行,当场景出现时。

所有为了完整性,下面是怎样手动执行这些动作示例:

Open URL

1
2
NSURL *URL = [NSURL URLWithString:@"http://nshipster.com"];
[[UIApplication sharedApplication] openURL:URL];

系统支持的URL协议包括:mailto: tel:sms:maps:

添加到Safari阅读列表

1
2
3
4
5
6
7
@import SafariServices;

NSURL *URL = [NSURL URLWithString:@"http://nshipster.com/uiactivityviewcontroller"];
[[SSReadingList defaultReadingList] addReadingListItemWithURL:URL
title:@"NSHipster"
previewText:@"..."
error:nil];

保存到相册

1
2
3
4
5
UIImage *image = ...;
id completionTarget = self;
SEL completionSelector = @selector(didWriteToSavedPhotosAlbum);
void *contextInfo = NULL;
UIImageWriteToSavedPhotosAlbum(image, completionTarget, completionSelector, contextInfo);

发送 SMS

1
2
3
4
5
6
7
8
9
@import MessageUI;

MFMessageComposeViewController *messageComposeViewController = [[MFMessageComposeViewController alloc] init];
messageComposeViewController.messageComposeDelegate = self;
messageComposeViewController.recipients = @[@"mattt@nshipster•com"];
messageComposeViewController.body = @"Lorem ipsum dolor sit amet";
[navigationController presentViewController:messageComposeViewController animated:YES completion:^{
// ...
}];

发送 Email

1
2
3
4
5
6
7
8
9
10
@import MessageUI;

MFMailComposeViewController *mailComposeViewController = [[MFMailComposeViewController alloc] init];
mailComposeViewController.mailComposeDelegate = self;
[mailComposeViewController setToRecipients:@[@"mattt@nshipster•com"]];
[mailComposeViewController setSubject:@"Hello"];
[mailComposeViewController setMessageBody:@"Lorem ipsum dolor sit amet" isHTML:NO];
[navigationController presentViewController:mailComposeViewController animated:YES completion:^{
// ...
}];

发送 Tweet

1
2
3
4
5
6
7
8
9
@import Social;

SLComposeViewController *tweetComposeViewController = [SLComposeViewController composeViewControllerForServiceType:SLServiceTypeTwitter];
[tweetComposeViewController setInitialText:@"Lorem ipsum dolor sit amet."];
[self.navigationController presentViewController:tweetComposeViewController
animated:YES
completion:^{
// ...
}];

IntentKit

当所有的这些都是印象深刻和非常有用,它在iOS中的活动范例特别缺乏,当和在Android中的丰富的Intents Model相比。

在Android中,apps能够注册不同的intents,来指示它们能够用在Maps,或者充当一个浏览器,能够被涉及到相关活动的默认app选中,比如指引方向,或者一个书签的URL。

然而iOS缺乏来支持这些的可拓展的基础架构,有一个叫IntentKit第三方库,作者@lazerwalker(因f*ingblocksyntax.com成名),这是一个有趣的例子关于我们怎样缩小这方面的差距。

IntentKit demo

通常,一个开发者可能已经做了很多第一步的工作,来决定是否某个app是否安装,然后怎样构造一个URL来支持某个特殊的活动。

IntentKit加强了连结到大多数流行的Web,Maps,Mail,Twitter,Facebook,和Google+客户的的逻辑,以和UIActivityViewController相似的UI。

任何寻找提升他们的分享体验到下一个级别的人都应该仔细看看这个。

在iOS长期生存能力作为一个平台取决于像UIActivityViewController类型的分享机制存上在很大的争论。正如俗语所言:“资讯应该免费”。任何阻碍方式统一的东西,最终都会失去一些没有的东西。

将来的景象是公共的远程视图控制器APIs带给我希望,对于将来在iOS平台上的分享功能。然而现在我们做的和UIActivityViewController相比糟糕得多。

前言

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

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

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

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

业绩

2016-2017年间一共发布17个版本,多次被Apple推荐,崩溃率小于0.2%,这就是给自己和公司交出最满意的答卷。

下面是Klook App上Apple的推荐页截图:

klook Apple App recommend

下面是Klook iMessage App上Apple的推荐页截图:

klook Apple iMessage App recommend

有时发现互联网真的很奇妙,他能让很多人迅速积累大量的财富成名走上人生的巅峰,过上自己想要的生活,只要你一个好的idea和一个强大的执行力。

这一年来个人技术博客一共写了46遍文章,也算一点小积蓄,希望来年继续坚持,能够影响和帮助更多的人,因为技术这条道路上坑实在是太多。

Github405次commit还算中规中矩,希望来年分享更多的有价值的东西给有需要的小伙伴,也留给自己方便以后查阅。

![github 2016 commit record](/images/2016_year_end_review/github 2016 commit record.png)

当然现在Github上的内容Fork和博客居多,原创开源项目还比较少,希望来年多输出一点东西。

未来

愿望这东西还真神奇,许一个试试吧,或许它真的实现了呢,就非常有趣了。
给自己定下的旅行清单,学习清单,阅读清单,电影清单基本都完成,这是一件非常棒的事情,为自己点个赞。单反已经从风光转人像,希望来年多出点片,新年即将来临,希望自己坚持一周2次的锻炼,保持精力充沛以便好投入到感情,生活中以及学习中,毕竟互联网是一门终生学习的职业,也是容易产生奇迹的职业,说不定一下个就是你呢。

来年希望和自己喜欢的人旅拍,多尝试一些没尝试过的事情,交更多有趣的朋友。干巴爹😁😁😁

主要涉及到Tips:

  1. 获取设备音量
  2. 静音模式失效
  3. 监听音量改变
  4. 设置设备音量
  5. 监听静音按钮
  6. 监听耳机拔插

获取设备音量

播放音频可以通过:

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中,通过UIColor CGColor方法返回的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.
}

无缝桥接类型

表1提供了一个在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

表1

参考资料

前言

随著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];
}];
}

最终效果

简介

此文用于总结,本人使用过或者收藏过的Github第三方类库,以便日后查阅,也便他人借鉴。

资料整理中不定期更新。。。

开源项目

APP相关

自动布局

  • Masonry

    最优雅的自动布局解决方案,支持链式编程

  • PureLayout

    又一强大的自动布局解决方案

网络请求

基础工具类以及Category

  • sstoolkit

    一个不错的工具包,提供各种比如编码、加密、字符串处理等等东西

  • BFKit Swift版本

    国外的一个大神写的很好用的分类,比较齐全

  • YYKit

    ibireme用心之作

  • BlocksKit

    为基础类提供Block支持,很好用

  • DateTools

    非常强大的日期处理工具

  • FlatUIKit

    扩展Foundation基本UI主件,扁平化风格

  • iOS-Categories

    iOS Objective-C Category, a collection of useful Objective-C Categories extending iOS Frameworks such as Foundation,UIKit,CoreData,QuartzCore,CoreLocation,MapKit Etc.

  • PinYin4Objc

    拼音解析

  • ZipArchive

    解压缩

  • Chameleon

    扁平颜色框架,可以创建非常漂亮的颜色

  • CYLTabBarController

    低耦合集成TabBarController

  • RKNotificationHub

    在右上方显示通知小角标

  • Material-Controls-For-iOS

    Google风格的控件

  • YYKit

    A collection of iOS components.

弹框

动画

Gif引擎

缓存

文本相关

加载进度

键盘类

抽屉

ScrollView相关

  • SwipeView

    SwipeView is a class designed to simplify the implementation of horizontal, paged scrolling views on iOS. It is based on a UIScrollView, but adds convenient functionality such as a UITableView-style dataSource/delegate interface for loading views dynamically, and efficient view loading, unloading and recycling.

  • SWTableViewCell

    自定义Cell侧滑动作

  • MGSwipeTableCell

    自定义Cell侧滑动作,效果多样

  • CHTCollectionViewWaterfallLayout

    瀑布流布局

  • TYSlidePageScrollView

    An easy solution to page views or controllers with header and page tabbar,footer

  • XLPagerTabStrip

    Android PagerTabStrip for iOS. nice

MJ系列

数据库

  • realm-cocoa

    跨平台轻量级数据库解决方案,励志取代Core Data & SQLite

  • fmdb

    sqlite实现面向对象的封装

图标库

  • PNChart

    提供各种图标样式,性能还非常不错

  • Charts

    Beautiful charts for iOS/tvOS/OSX! The Apple side of the crossplatform MPAndroidChart.

图片处理以及展示相关

日历选择

  • FSCalendar

    iOS7+风格的日历控件,非常漂亮

音频/视频

内购/推广

  • IAPHelper

    in app purchases helper for iOS

  • DAAppsViewController

    应用推广界面,填写合作app的appleId即可

  • TAPromotee

    交叉推广应用是你可以免费实现的最佳市场推广策略之一。

存储相关

实用控件

辅助类

  • Surge

    利用Accelerate高性能数学计算框架

  • KMCGeigerCounter

    显示当前动画的帧率

  • XNGMarkdownParser

    Markdown解析器,将Markdown格式的字符串解析成NSAttributedString格式的字符串

  • TransformerKit

    封装了一些常用的NSValueTransformer

  • RuntimeBrowser

    Objective-C Runtime Browser, for Mac OS X and iOS

  • iOS-Runtime-Headers

    iOS Objective-C headers as derived from runtime introspection

  • appledoc

    Objective-C代码,苹果风格文档生成器

  • Markingbird

    Markdown processor written in Swift (translation of MarkdownSharp)

Swift

其它

  • FreeCodeCamp

    The https://FreeCodeCamp.com open source codebase and curriculum. Learn to code and help nonprofits.

  • libextobjc

    拓展了一些Cocoa类库

  • ParseSourceCodeStudy

    Facebook开源的Parse源码分析【系列】

  • hugo

    使用Google Go语言书写的静态博客网站生成器

  • CocoaPods

    Cocoa包依赖管理工具

  • MDCSwipeToChoose

    实现某App,左滑不喜欢,右滑喜欢的卡片堆叠功能

  • Dash-Plugin-for-Xcode

    Xcode的一个插件,整合Dash,方便快速查阅文档

  • iOS 开发面试问题

    汇总一些常见的iOS面试问题

  • XMPPFramework

    XMPP即时通信协议框架

  • WebViewJavascriptBridge

    Objective-CJavaScriptUIWebViews/WKWebView之间交互解决方案

  • articles-1

    Articles for objccn.io. objc.io的完整、准确、优雅的中文翻译版本

  • articles

    Weekly articles for NSHipster.com

  • ebook

    收集了一些coding相关的电子书

  • dev-blog

    nixzhu的一些 iOS / Web 开发相关的翻译或原创博客文章

  • KVOController

    Simple, modern, thread-safe key-value observing for iOS and OS X.

  • LBXScan

    二维码、扫码、扫一扫、ZXing和ios系统自带扫码封装,扫码界面效果封装

参考链接: