Swift 3.0 去掉 C 风格循环后怎么办?

From: https://swiftcafe.io/2016/07/18/swift-loop/

Swift 3.0 版本将会去掉沿用已经的 C 风格循环语法, 又是向现代开发语言的一次迈进, 咱们就来看看没了 C 风格循环我们还有什么选择, 看过之后你会不会感觉 C 风格循环在 Swift 中确实有点多余呢?

C 风格循环

关于 C 风格循环, 不我们过多介绍了, 就是类似这样的语句:

let numberList = [1, 2, 3, 4, 5]

for var i = 0; i < numberList.count; i++ {

}

如今这样的语法在新版本的 Swift 中即将成为历史了, C 风格的循环语法可能是大家最熟悉的, 大家会不会觉得突然去掉这个语法有些不适应呢? 咱们再来看看 Swift 3 中的替代方案。

for .. in 语法

第一个替代方案, 我们可以使用 for .. in 这样的语法:

let numberList = [1, 2, 3, 4, 5]

var result = “”

for num in numberList {

result += “(num) “

}

这样就完成了对数组的遍历了, 但是还有另一个情况, 如果我们想知道每次遍历的索引怎么办呢, 还有一种方法:

for num in numberList.enumerate() {

result += “[(num.index)](num.element) “

}

我们可以使用这个集合类型的 enumerate 方法,将这个数组的索引和对应的元素都取了出来,然后我们在循环中就可以对索引项进行引用了, num.index 和 num.element 分别代表对应的索引和元素。

上面这个循环我们还可以再改写一下:

for (index, item) in numberList.enumerate() {

result += “[(index)](item) “

}

不难看出,其实循环中的每一项都是一个元组(Tuple),这个元组的第一项是当前的索引, 第二项是当前的数组元素。 那么我们就可以推理出, enumerate 函数其实就是对 numberList 数组做了一个变换,原来它是一个 Int 类型的数组,经过变换后,成为了 (Int, Int) 元组类型的数组。

是不是这么回事呢? 查看 enumerate 方法的文档后, 看到它的定义是这样的:

func enumerate() -> EnumerateSequence>

比我们想象的要复杂些, EnumerateSequence 是个什么鬼, 让我们再来看看它的文档定义:

The SequenceType returned by enumerate(). EnumerateSequence is a sequence of pairs (n, x), where ns are consecutive Ints starting at zero, and xs are the elements of a Base SequenceType

仔细看下, 其实跟我们理解的还是差不多的, 它只不过是对集合类的一个集成, 这个集合每一项是一个元组 (n, x) , n 代表索引, x 代表数组元素。

那么,我们还可以做点更有意思的事情:

for (index, item) in numberList.enumerate().reverse() {

result += “[(index)](item) “

}

调用 enumerate, 之后再调用 reverse 方法, 我们就可以对一个数组进行反向遍历。

for (index, item) in numberList.enumerate().reverse() {

result += “[(index)](item) “

}

我们还可以:

for (index, item) in numberList.enumerate().filter({ (index, item) in index % 2 == 0}) {

result += “[(index)](item) “

}

调用 filter 函数,过滤某些索引, 只遍历符合条件的那些元素。

当然, 我们还可以做的更多更多, 大家有兴趣可以看看 SequenceType 的文档,把你的新思路回复给大家。 http://swiftdoc.org/v2.2/protocol/SequenceType

区间(Range)循环

除了刚才咱们说的这些, Swift 还提供了更方便的循环语法, 叫做 Range 循环。 比如这样:

var rs = “”;

for i in 0…10 {

rs += “(i)”

}

print(rs)

这个语句会输出 0 到 10 之间的所有数字, 0…10 这个表示 Range 区间的范围。 当然,对于我们刚才的数组遍历来说, 一般数组索引都是数组长度减去 1, 用这个区间处理起来就会比较麻烦, 不过好在 Swift 给我们提供了另外一种 Range 方法:

for i in 0..<numberList.count {

rs += “(i)”

}

这次我们换成了 0..<numberList.count, 这种形式会排除闭区间最后那个数组,然后我们就可以在循环中用索引进行访问啦(注意符号 ..< 两边不要有空格)。

结尾

好了,今天跟大家分享的内容就这么多。C 风格的循环语句其实更多是我们的一个长期养成的习惯问题。 世界的一切都在进步发展,包括开发语言也是一样。 看了 Swift 提供的循环语法, 你对 C 风格循环还有没有存在的必要时什么看法呢。 早些拥抱趋势和变化总是好的。

如果你觉得这篇文章有帮助,还可以关注微信公众号 swift-cafe,会有更多我的原创内容分享给你~

本站文章均为原创内容,如需转载请注明出处,谢谢。


推荐关注微信公众平台
swift-cafe

UM集成坑点

/// 此笔记级一些集成UM遇到的坑
分享面板无法弹出 最终找到这个原因一坑

1
2
3
4
5
6
7
8
9
10
11
问题可能有下面的原因:
1. 创建Xcode项目会默认添加Main.storyboard作为Main Interface(General - Deployment Info),也就是项目的主Window
2. 如果没使用Main.storyboard而又另外在AppDelegate中创建了UIWindow对象,如
self.window = [[UIWindow alloc] initWithFrame:[UIScreen mainScreen].bounds]
如果项目中同时出现Main Interface以及代码创建UIWindow会导致分享面板无法正常弹出,解决方法是移除其一即可。如果移除了Main.storyboard,需要clean工程后再重新运行。
使用presentViewController或其他无法显示分享面板的情况,参考文档链接修改父窗口回调为self.view或其他指定视图,文档链接

UM 取消授权

1
2
3
UMSocialManager.default().cancelAuth(with: UMSocialPlatformType.QQ, completion: nil)
UMSocialManager.default().cancelAuth(with: UMSocialPlatformType.sina, completion: nil)
UMSocialManager.default().cancelAuth(with: UMSocialPlatformType.wechatSession, completion: nil)

下面这点算是QQ的坑点 , 统一id 问题

Unionid接口权限申请流程:目前只支持同一个开发者号码下的应用进行打通。如有需要,可以发邮件到connect@qq.com申请,提供应用类型、信息(APPID和APPKEY),附上营业执照图片、网站备案截图(若有网站应用需要提供该项)即可。打通后同一个QQ登录不同APP ID应用后返回的unionid一致。具体打通事宜后续工作人员会通过邮件确认,请在1~5个工作日留意邮件,以邮件回复为准。
http://wiki.connect.qq.com/%E5%BC%80%E5%8F%91%E8%80%85%E5%8F%8D%E9%A6%88

iOS中的设计模式——单例(Singleton)

swift

1
2
3
4
5
6
7
8
9
class NetTool: NSObject {
/// 实例创建
fileprivate static let instance = NetTool()
class var shared:NetTool{
return instance
}
}

From: http://ibloodline.com/articles/2016/09/19/singleton.html

设计模式

Comments

单例模式

单例模式(Singleton:保证一个类仅有一个实例,并提供一个访问它的全局访问点。

单例模式应该是设计模式中最简答的形式了。这一模式的意图是让类的一个对象成为系统中唯一的实例。

类图

Singleton

使用场景

  • 类只能有一个实例,而且必须从一个为人熟知的访问点对其进行访问,比如工厂方法
  • 这个唯一的实例只能通过子类化进行扩展,而且扩展的对象不会破坏客户端代码。

优点:

  1、提供了对唯一实例的受控访问。

  2、由于在系统内存中只存在一个对象,因此可以节约系统资源,对于一些需要频繁创建和销毁的对象单例模式无疑可以提高系统的性能。

  3.因为单例模式的类控制了实例化的过程,所以类可以更加灵活修改实例化过程。

缺点:

  1、由于单利模式中没有抽象层,因此单例类的扩展有很大的困难。

  2、单例类的职责过重,在一定程度上违背了“单一职责原则”。

使用方式

先看C++中的实现:

class Singlenton
{
public:
    static Singlenton *Instance();

protected:
    Singlenton();

private:
    static Singlenton *_instance;
};

Singlenton *Singlenton::_instance = NULL;

Singlenton *Singlenton::Instance()
{
    if (_instance == NULL) {
        _instance = new Singlenton;
    }
    return _instance;
}

OC下:

//Singleton.h
@interface Singleton : NSObject
+ (Singleton *)sharedInstance;
@end

//Singleton.m
@implementation Singleton
static Singleton * sharedSingleton = nil;
+ (Singleton *) sharedInstance {
    if (sharedSingleton == nil) {
        sharedSingleton = [[Singleton alloc] init];
    }
    return sharedSingleton;
}
@end

上面的实现是有问题的。首先,如果客户端使用不同的方式来初始化单例,则有可能出现多个实例的情况。另外,这样的实现也不是线程安全的。改进:

@implementation Singleton
static id sharedSingleton = nil;
+ (id)allocWithZone:(struct _NSZone *)zone {
    if (!sharedSingleton) {
        static dispatch_once_t onceToken;
        dispatch_once(&onceToken, ^{
            sharedSingleton = [super allocWithZone:zone];
        });
    }
    return sharedSingleton;
}
- (id)init {
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        sharedSingleton = [super init];
    });
    return sharedSingleton;
}
+ (instancetype)sharedInstance {
    return [[self alloc] init];
}
+ (id)copyWithZone:(struct _NSZone *)zone {
    return sharedSingleton;
}
+ (id)mutableCopyWithZone:(struct _NSZone *)zone {
    return sharedSingleton;
}
@end

当然对于懒癌患者来讲,每个单例都写这样的实现实在太不可接受了,我们把它抽取成宏:

// .h文件的实现
#define SingletonH(methodName) + (instancetype)shared##methodName;

// .m文件的实现
#if __has_feature(objc_arc) // 是ARC
#define SingletonM(methodName) \
static id _instace = nil; \
+ (id)allocWithZone:(struct _NSZone *)zone \
{ \
if (_instace == nil) { \
static dispatch_once_t onceToken; \
dispatch_once(&onceToken, ^{ \
_instace = [super allocWithZone:zone]; \
}); \
} \
return _instace; \
} \
\
- (id)init \
{ \
static dispatch_once_t onceToken; \
dispatch_once(&onceToken, ^{ \
_instace = [super init]; \
}); \
return _instace; \
} \
\
+ (instancetype)shared##methodName \
{ \
return [[self alloc] init]; \
} \
+ (id)copyWithZone:(struct _NSZone *)zone \
{ \
return _instace; \
} \
\
+ (id)mutableCopyWithZone:(struct _NSZone *)zone \
{ \
return _instace; \
}

#else // 不是ARC

#define SingletonM(methodName) \
static id _instace = nil; \
+ (id)allocWithZone:(struct _NSZone *)zone \
{ \
if (_instace == nil) { \
static dispatch_once_t onceToken; \
dispatch_once(&onceToken, ^{ \
_instace = [super allocWithZone:zone]; \
}); \
} \
return _instace; \
} \
\
- (id)init \
{ \
static dispatch_once_t onceToken; \
dispatch_once(&onceToken, ^{ \
_instace = [super init]; \
}); \
return _instace; \
} \
\
+ (instancetype)shared##methodName \
{ \
return [[self alloc] init]; \
} \
\
- (oneway void)release \
{ \
\
} \
\
- (id)retain \
{ \
return self; \
} \
\
- (NSUInteger)retainCount \
{ \
return 1; \
} \
+ (id)copyWithZone:(struct _NSZone *)zone \
{ \
return _instace; \
} \
\
+ (id)mutableCopyWithZone:(struct _NSZone *)zone \
{ \
return _instace; \
}

使用:

//SmartSingleton.h
@interface SmartSingleton : NSObject
SingletonH(SmartSingleton)
@end

//SmartSingleton.m
@implementation SmartSingleton
SingletonM(SmartSingleton)
@end

//客户端调用
Singleton *singleton = [Singleton sharedInstance];
NSLog(@"%@", singleton);

SmartSingleton *smartSingleton = [SmartSingleton sharedSmartSingleton];
NSLog(@"%@", smartSingleton);

Cocoa中的单例

Cocoa中最常见的单例类是UIApplication类。它提供了一个控制并协调应用程序的集中点。

每个应用程序有且只有一个UIApplication实例。它由UIApplicationMain函数在应用程序启动时创建为单例对象。之后,对同一UIApplication实例可以通过sharedUIApplication类方法进行访问。

UIApplication对象为应用程序处理许多内务管理任务(housekeeping task),包括传入的用户时间的最初路由,以及为UIControl分发动作消息给合适的目标对象。它还卫华应用程序中打开的所有UIWindow对象的列表。应用程序对象总是被分配一个UIApplicationDelegate对象。应用程序将把重要的运行时事件通知给它,比如iOS应用程序中的应用程序启动、内存不足警告、应用程序终止和后台进程执行。这让代理(delegate)有机会作出适当的响应。

NSUserDefaultNSFileManager等也是常见的单例实现。

总结

只要应用程序需要用集中式的类来协调其服务,这个类就应生成单一的实例。

代码

文章中的代码都可以从我的GitHub DesignPatterns找到。

iOS成员属性和成员变量的区别

From: http://www.jianshu.com/p/55f781f8c915

一、@property 和@synthesizer

在objective-c 1.0中,我们为interface同时声明了属性和底层实例变量,那时,属性是oc语言的一个新的机制,并且要求你必须声明与之对应的实例变量,例如:

@interface MyViewController :UIViewController
{
    UIButton *myButton;
}
@property (nonatomic, retain) UIButton *myButton;
@end

在objective-c 2.0中,@property它将自动创建一个以下划线开头的实例变量。因此,在这个版本中,我们不再为interface声明实例变量。变成我们常见的形式

@interface MyViewController :UIViewController
@property (nonatomic, retain) UIButton *myButton;
@end

在MyViewController.m文件中,编译器也会自动的生成一个实例变量_myButton。那么在.m文件中可以直接的使用_myButton实例变量,也可以通过属性self.myButton.都是一样的。

注意这里的self.myButton其实是调用的myButton属性的getter/setter方法。这与C++中点的使用是有区别的,C++中的点可以直接访问成员变量(也就是实例变量)。

例如在oc的.h文件中有如下代码

@interface MyViewController :UIViewController
{
    NSString *name;
}

.m文件中,self.name 这样的表达式是错误的。xcode会提示你使用->,改成self->name就可以了。因为oc中点表达式是表示调用方法,而上面的代码中没有name这个方法。所以在oc中点表达式其实就是调用对象的setter和getter方法的一种快捷方式。

你可能还见过这种写法

#import "ViewController.h"

@interface ViewController ()
@property (nonatomic, strong) UIButton *myButton;
@end

@implementation ViewController
@synthesize myButton;

@synthesize 语句只能被用在 @implementation 代码段中,@synthesize的作用就是让编译器为你自动生成setter与getter方法,@synthesize 还有一个作用,可以指定与属性对应的实例变量,例如@synthesize myButton = xxx;那么self.myButton其实是操作的实例变量xxx,而不是_myButton了。

如果.m文件中写了@synthesize myButton;那么生成的实例变量就是myButton;如果没写@synthesize myButton;那么生成的实例变量就是_myButton。所以跟以前的用法还是有点细微的区别。

二、类别中的属性property

类与类别中添加的属性要区分开来,因为类别中只能添加方法,不能添加实例变量。经常会在ios的代码中看到在类别中添加属性,这种情况下,是不会自动生成实例变量的。比如在:UINavigationController.h文件中会对UIViewController类进行扩展

@interface UIViewController (UINavigationControllerItem)
@property(nonatomic,readonly,retain) UINavigationItem *navigationItem;
@property(nonatomic) BOOL hidesBottomBarWhenPushed;
@property(nonatomic,readonly,retain) UINavigationController *navigationController;
@end

这里添加的属性,不会自动生成实例变量,这里添加的属性其实是添加的getter与setter方法。注意一点,匿名类别(匿名扩展)是可以添加实例变量的,非匿名类别是不能添加实例变量的,只能添加方法,或者属性(其实也是方法),常用的扩展是在.m文件中声明私有属性和方法。 Category理论上不能添加变量,但是可以使用rRuntime机制来弥补这种不足。

#import
static const void * externVariableKey =&externVariableKey;
@implementation NSObject (Category)
@dynamic variable;
- (id) variable
{
       return objc_getAssociatedObject(self, externVariableKey);
}
- (void)setVariable:(id) variable
{
    objc_setAssociatedObject(self, externVariableKey, variable, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}

三、@private、@protect、@public

@protected是受保护的,只能在本类及其子类中访问,在{}声明的变量默认是@protect
@private是私有的,只能在本类访问
@public公开的,可以被在任何地方访问。
在头文件.h中:

@interface ViewController : UIViewController
{
// 成员变量
        @public
            NSString* publicString;

        @protected
            NSString* protectedString;

        @private
            NSString* privateString;
}
//属性变量
@property (nonatomic,strong) NSArray *propertyString;
@end
  • 成员变量用于类内部,无需与外界接触的变量。
  • 根据成员变量的私有性,为了方便访问,所以就有了属性变量。属性变量是用于与其他对象交互的变量。(属性变量的好处就是允许让其他对象访问到该变量。当然,你可以设置只读或者可写等,设置方法也可自定义。)

一些建议:
1.如果只是单纯的private变量,最好声明在implementation里.
2.如果是类的public属性,就用property写在.h文件里
3.如果自己内部需要setter和getter来实现一些东西,就在.m文件的类目里用property来声明

.h中的interface的大括号{}之间的实例(成员)变量,.m中可以直接使用;
.h中的property(属性)变量,.m中需要使用self.propertyVariable的方式使用propertyVariable变量

四、成员变量和成员属性的关系

  1. 属性对成员变量扩充了存取方法 .
  2. 属性默认会生成带下划线的成员变量 .
  3. 但只声明了变量,是不会有属性的,可以通过以下代码证明
    在Person.h 头文件中

    @interface Person : NSObject {
    @private
    //name为私有成员变量
    NSString name;
    }
    // age 为成员属性
    @property (nonatomic ,copy) NSString
    age;

在viewController.m 中,通过RunTime机制获得对象的所有成员变量和成员属性。

Person *p = [Person new];
unsigned int count = 0; //count记录变量的数量

// 获取类的所有成员变量
Ivar *members = class_copyIvarList([Person class], &count);
for (int i = 0; i < count; i++) {
    Ivar ivar = members[i];
    // 取得变量名并转成字符串类型
    const char *memberName = ivar_getName(ivar);
    NSLog(@"变量名 = %s",memberName);
}
// 获取类的所有成员属性
objc_property_t *properties =class_copyPropertyList([Person class], &count);
for (int i = 0; i<count; i++)
{
    objc_property_t property = properties[i];
    const char* char_f =property_getName(property);
    NSString *propertyName = [NSString stringWithUTF8String:char_f];
    NSLog(@"属性名 = %@",propertyName);
}

打印结果为

2016-08-12 11:31:50.225 modifyPrivate[777:143231] 变量名 = name
2016-08-12 11:31:50.226 modifyPrivate[777:143231] 变量名 = _age
2016-08-12 11:31:50.226 modifyPrivate[777:143231] 属性名 = age

关于Block的定义,和作为参数的写法

关于Block的定义,和作为参数的写法
转载 : http://www.dahuangphone.com/dispbbs.asp?boardid=8&id=85&page=3&star=1

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
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
Block的定义
Block的格式: ^ + 返回值类型(可以省略) + (参数)(如果没有参数,可以省略) + {表达式}
Block变量格式: 返回值类型(不可省略, 最少void) + (^变量名称) + (参数) (不可省略, 至少()). 格式和函数指针很相似,只是把*改成了^.
int (^blockName)(int, NSString*)=^(int para1, NSString *para2){
return 1;
};
//int (^blockName)(int, NSString*)的意思是要定义一个名字为"blockName"的block,它据有两个参数,具有一个int类型返回值.
void (^blockName)()=^{
};//如果没有参数,可省略如上面写法, 如果没有返回值, 必须要写void
使用typedef: (Block声明比较复杂, 建议使用这种方式生命Block)
typedef int(^blockName1)(int,NSString*);
blockName1 bn=^(int para1, NSString *para2){
return 1;
};
无参数情况:
typedef int(^blockName1)();
blockName1 bn=^{
return 1;
};
.......................................................................
作为函数参数写法:
c函数:
参数
typedef int(^ABlock)();
void cFunc(void(^blockName)(), ABlock block){//两种写法都可以
}
返回值写法
int (^fun())(int){
return ^(int cout){ return cout;};
}
ABlock fun(){
return ^(int cout){ return cout;};
}
OC函数
参数
-(void)OCFunc:(void(^)())blockName andOtherBlock:(ABlock)block{ //注意第一种写法的特别之处, OC函数要求变量类型和形参名分开, 所以写法和C不同
}
@property 写法
@property(nonatomic,copy) int (^block)(int a); //使用c的方式, 不能使用OC函数形参的写法.
@property (nonatomic,copy) Block blockName;
以Block作为方法返回值的写法:
-(int (^)(int))blockBack{ //和c的写法是不同的, 需要注意
return ^(int cout){ return cout;};
}
block不同其它变量的原因在于它不是一个单一变量, 而是一个方法,
我们要传递的是一个代码块,并且这个代码块可以存在参数,
这个参数并不是在定义block的时候就赋予值, 而是我们在实际运行block的时候才赋予值.
因此对于有参数的block,当我们传递过去的时候, 它的需要接收方提供相应的参数才能运行,
这么做我们就可以在A类为B类将来会发生的事件提前做好处理的方法,即使我们还没有这些事件的具体参数.
某种意义上将这样就不需要两者之间的委托关系.
委托关系就是B类发生一个事件后,通知A类,让A类再针对这个事件进行一些处理
而使用block,则是A已经提前将这个事件的处理方法告诉了B类, 等时间发生的时候, B类无需通知A类, 直接运行实现设置好的处理方法(block)即可.
如果你在运行一个方法的时候又想告诉这个方法在某一特定情况你还要怎么做的话, 就可以使用Block.
GCD:
GCD主要使用block来代替委托模式,使程序变得简洁,同时运行效率也得到提高.
dispatch_async(dispatch_get_global_queue(0, 0), ^{
static int i=0;
while (i<20) {
dispatch_async(dispatch_get_main_queue(), ^{
_label.text=[NSString stringWithFormat:@"%d",i++]; //UI的更新必须要在主线程完成
});
[NSThread sleepForTimeInterval:1];
}
});
NSLog(@"s");
这个函数意思是更新label的显示.每秒钟更新一次, 如果我不实用异步更新直接使用一个方法如下:
-(void)runLabel{
static int i=0;
while (i<20) {
_label.text=[NSString stringWithFormat:@"%d",i++];
[NSThread sleepForTimeInterval:1];
}
}
这回导致整个程序无法响应其它事件.
如果使用NSThread 完成则需要委托, 非常繁琐.
[此贴子已经被作者于2013/6/27 12:18:57编辑过]
2楼 dahuangphone 发表于:2013/4/30 1
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
block的基础知识
block是一个特殊的OC对象, 它建立在栈上, 而不是堆上, 这么做一个是为性能考虑,还有就是方便访问局部变量.
默认情况下block使用到的局部变量都会被复制,而不是保留.
所以它无法改变局部变量的值.
如果在变量面前加上__block, 那么编译器回去不会复制变量, 而是去找变量的地址, 通过地址来访问变量, 实际上就是直接操作变量.
另外块是在栈上分配的, 所以一旦离开作用域, 就会释放, 因此如果你要把快用在别的地方, 必须要复制一份.
所以在属性定义一个快的时候需要使用copy: @property (nonatomic, copy) void (^onTextEntered)(NSString *enteredText);
块是不能保留的, retain对块没有意义.
用块遍历字典:
[dict enumerateKeysAndObjectsUsingBlock:^(id key, id obj, BOOL *stop) {
NSLog(@"%@,%@",key,obj);
}
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
ARC下Block何时会从栈自动被复制到推, 以及__block和__weak的使用问题
由于Block是默认建立在栈上, 所以如果离开方法作用域, Block就会被丢弃, 在非ARC情况下, 我们要返回一个Block ,需要 [Block copy];
在ARC下, 以下几种情况, Block会自动被从栈复制到堆:
1.被执行copy方法
2.作为方法返回值
3.将Block赋值给附有__strong修饰符的id类型的类或者Blcok类型成员变量时
4.在方法名中含有usingBlock的Cocoa框架方法或者GDC的API中传递的时候.
对于非ARC下, 为了防止循环引用, 我们使用__block来修饰在Block中实用的对象:
__block id blockSelf=self;
self.blk=^{
NSLog(@"%@",blockSelf); //在非ARC下对于栈上的_block对象, Block不会对其复制, 仅仅使用, 不会增加引用计数.
};
对于ARC下, 为了防止循环引用, 我们使用__weak来修饰在Block中实用的对象:
__weak id weakSelf=self;
self.blk=^{
NSLog(@"%@",weakSelf);
};
如果要在ARC下, 为了防止循环引用, 使用__block来修饰在Block中实用的对象,仍然会被retain, 所以需要多做一些设置
__block id blockSelf=self;
self.blk=^{
NSLog(@"%@",blockSelf);
self.blk=nil; //blk被释放, blk只有的blockSelf也就被释放了
};
blk(); //并且一定要运行一次, 否则不能被释放
这样就使blk断开了与blockSelf的持有关系.
这么多好处是可以自己控制对self的持有时间.
不过在最新的ios版本中, 这些会始终被已叹号形式提示存在循环引用问题.
这种书写方式不被推荐. 除非你要在block中修改__block的指针指向.
其实我们用使用__weak修饰符, 只是不能修改对象本身, 但是可以修改对象的属性.

关于AFNetworking3.0+的使用

From: http://www.jianshu.com/p/5e187c9d389b

  1. 为了让刚接触AFNetworking的开发者能够顺序上手,本文专门对AFNetworking的使用方法进行了归纳总结,同时方便自己以后的学习。
  2. 由于本人水平有限, 极大可能会出现错误,所以阅读本文过程中如发现异议,还请各位耐心指教,共同探讨学习
  3. 本文参考各位前辈的经验,感谢各位大神的分享

AFNetworking 1.0建立在NSURLConnection的基础API之上 ,AFNetworking 2.0开始使用NSURLConnection的基础API ,以及较新基于NSURLSession的API的选项。 AFNetworking 3.0现已完全基于NSURLSession的API,这降低了维护的负担,同时支持苹果增强关于NSURLSession提供的任何额外功能。由于Xcode 7中,NSURLConnection的API已经正式被苹果弃用。虽然该API将继续运行,但将没有新功能将被添加,并且苹果已经通知所有基于网络的功能,以充分使NSURLSession向前发展。

3.1、 NSURLConnection的介绍

关于NSURLConnection的使用,本文不做详细的介绍,具体参考简书某位大神的介绍http://www.jianshu.com/p/f291ee58c012

3.2、 NSURLSession的介绍

NSURLSession的优点:

1.后台上传和下载。当你的程序退出了也能进行网络操作,这对用户和APP来说都是个好消息,不用运行APP就可以下载和上传,这样更节约手机电量。

2.能够暂停和恢复网络操作。不需要使用NSOperation就可以实现暂停、继续、重启等操作。

3.可配置的容器。

4.可以子类化并且可以设置私有存储方式。可以修改数据的存储方式和存储位置。

5.改进了授权处理机制。

6.代理更强大。

7.通过文件系统上传和下载。

创建NSURLSession对象的方法
1.使用静态的sharedSession方法,该类使用共享的会话,该会话使用全局的Cache,Cookie和证书。
+ (NSURLSession *)sharedSession;  
2.通过sessionWithConfiguration:方法创建对象,也就是创建对应配置的会话,与NSURLSessionConfiguration合作使用。
(NSURLSession *)sessionWithConfiguration:(NSURLSessionConfiguration *)configuration;  
3.通过sessionWithConfiguration:delegate:delegateQueue方法创建对象,二三两种方式可以创建一个新会话并定制其会话类型。该方式中指定了session的委托和委托所处的队列。当不再需要连接时,可以调用Session的invalidateAndCancel直接关闭,或者调用finishTasksAndInvalidate等待当前Task结束后关闭。这时Delegate会收到URLSession:didBecomeInvalidWithError:这个事件。Delegate收到这个事件之后会被解引用。
+ (NSURLSession *)sessionWithConfiguration:(NSURLSessionConfiguration *)configuration delegate:(id <NSURLSessionDelegate>)delegate delegateQueue:(NSOperationQueue *)queue;
NSURLSessionTask支持的三种任务

加载数据/下载/上传
NSURLSessionTask类
NSURLSessionTask是一个抽象类,它有三个子类

1.NSURLSessionDataTask
2.NSURLSessionUploadTask
3.NSURLSessionDownloadTask

这三个类封装了应用程序的三个基本网络任务:获取数据,比如JSON或XML,以及上传和下载文件。
继承关系如下:

屏幕快照 2016-08-12 13.36.02.png

NSURLSessionConfiguration配置信息

NSURLSessionConfiguration用于配置会话的属性,可以通过该类配置会话的工作模式:

NSURLSessionConfiguration *config = [NSURLSessionConfiguration defaultSessionConfiguration];
// 超时时间
config.timeoutIntervalForRequest = 10;
// 是否允许使用蜂窝网络(后台传输不适用)
config.allowsCellularAccess = YES;
// 还有很多可以设置的属性
//关于配置信息的实例化方法大概有三种
/*
//默认会话模式(default):工作模式类似于原来的NSURLConnection,使用的是基于磁盘缓存的持久化策略,使用用户keychain中保存的证书进行认证授权。
+ (NSURLSessionConfiguration *)defaultSessionConfiguration;
//瞬时会话模式(ephemeral):该模式不使用磁盘保存任何数据。所有和会话相关的caches,证书,cookies等都被保存在RAM中,因此当程序使会话无效,这些缓存的数据就会被自动清空。
+ (NSURLSessionConfiguration *)ephemeralSessionConfiguration;
//后台会话模式(background):该模式在后台完成上传和下载,在创建Configuration对象的时候需要提供一个NSString类型的ID用于标识完成工作的后台会话。
+ (NSURLSessionConfiguration *)backgroundSessionConfigurationWithIdentifier:(NSString *)identifier
*/
NSURLSessionTask相关方法
//suspend可以让当前的任务暂停
- (void)suspend;
//resume方法不仅可以启动任务,还可以唤醒suspend状态的任务
- (void)resume;
//cancel方法可以取消当前的任务,你也可以向处于suspend状态的任务发送cancel消息,任务如果被取消便不能再恢复到之前的状态.
- (void)cancel;

3.2.1、NSURLSessionDataTask简单GET请求

如果请求的数据比较简单,也不需要对返回的数据做一些复杂的操作.那么我们可以使用带block

// 快捷方式获得session对象
NSURLSession *session = [NSURLSession sharedSession];
/*GET请求将参数拼接在 url 后面
    网络接口 和 参数 以 ? 分隔. 参数和参数之间以 & 符号分隔.注意删除最后一个 & 符号.
    如:http://127.0.0.1/login.php?username=zhangsan&password=zhang
*/
NSURL *url = [NSURL URLWithString:@"http://www.daka.com/login?username=daka&pwd=123"];
// GET请求直接根据url实例化网络任务
/*
     第一个参数:请求路径:内部会自动将路径包装成请求对象
     第二个参数:completionHandler回调(请求完成【成功|失败】的回调)
     data:响应体信息(期望的数据)
     response:响应头信息,主要是对服务器端的描述
     error:错误信息,如果请求失败,则error有值
*/
NSURLSessionTask *task = [session dataTaskWithURL:url completionHandler:^(NSData *data, NSURLResponse *response, NSError error) {
       // 默认是子线程.
        NSLog(@"%@",[[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding]);
}];
// 开启任务
[task resume];
3.2.2、NSURLSessionDataTask简单POST请求

POST和GET的区别就在于request,所以使用session的POST请求和GET过程是一样的,区别就在于对request的处理.

NSURL *url = [NSURL URLWithString:@"http://www.daka.com/login"];
//创建可变请求对象
NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url];
//POST请求将参数添加在请求体中
//设置请求方法
request.HTTPMethod = @"POST";
//设置请求体
request.HTTPBody = [@"username=daka&pwd=123" dataUsingEncoding:NSUTF8StringEncoding];

NSURLSession *session = [NSURLSession sharedSession];
/*
     第一个参数:请求对象
     第二个参数:completionHandler回调(请求完成【成功|失败】的回调)
     data:响应体信息(期望的数据)
     response:响应头信息,主要是对服务器端的描述
     error:错误信息,如果请求失败,则error有值
*/
// 由于要先对request先行处理,我们通过request初始化task
NSURLSessionTask *task = [session dataTaskWithRequest:request
                                   completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) 
{
       // 默认是子线程.
        NSLog(@"%@",[[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding]);
}];
[task resume];
3.2.3、NSURLSessionDataTask的NSURLSessionDataDelegate代理方法

NSURLSession提供了block方式处理返回数据的简便方式,但如果想要在接收数据过程中做进一步的处理,仍然可以调用相关的协议方法.NSURLSession的代理方法和NSURLConnection有些类似,都是分为接收响应、接收数据、请求完成几个阶段.

// 使用代理方法需要设置代理,但是session的delegate属性是只读的,要想设置代理只能通过这种方式创建session
NSURLSession *session = [NSURLSession sessionWithConfiguration:[NSURLSessionConfiguration defaultSessionConfiguration] delegate:self delegateQueue:[[NSOperationQueue alloc] init]];
// 创建任务(因为要使用代理方法,就不需要block方式的初始化了)
NSURLSessionDataTask *task = [session dataTaskWithRequest:[NSURLRequest requestWithURL:[NSURL URLWithString:@"http://www.daka.com/login?userName=daka&pwd=123"]]];
// 启动任务
[task resume];
//对应的代理方法如下:
// 1.接收到服务器的响应
- (void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask didReceiveResponse:(NSURLResponse *)response completionHandler:(void (^)(NSURLSessionResponseDisposition))completionHandler { 
       // 允许处理服务器的响应,才会继续接收服务器返回的数据      
      completionHandler(NSURLSessionResponseAllow);
}
// 2.接收到服务器的数据(可能调用多次)
- (void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask didReceiveData:(NSData *)data { 
       // 处理每次接收的数据
}
// 3.请求成功或者失败(如果失败,error有值)
- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didCompleteWithError:(NSError *)error 
{
       // 请求完成,成功或者失败的处理
}
Tips:
关键点在代码注释里面都有提及,重要的地方再强调一下:

如果要使用代理方法,需要设置代理,但从NSURLSession的头文件发现session的delegate属性是只读的.因此设置代理要通过session的初始化方法赋值:sessionWithConfiguration:delegate:delegateQueue:其中:
configuration参数(文章开始提到的)需要传递一个配置,我们暂且使用默认的配置[NSURLSessionConfiguration defaultSessionConfiguration]就好(后面会说下这个配置是干嘛用的);
delegateQueue参数表示协议方法将会在哪个队列(NSOperationQueue)里面执行.
NSURLSession在接收到响应的时候要先对响应做允许处理:completionHandler(NSURLSessionResponseAllow);,才会继续接收服务器返回的数据,进入后面的代理方法.值得一提的是,如果在接收响应的时候需要对返回的参数进行处理(如获取响应头信息等),那么这些处理应该放在前面允许操作的前面.
3.2.4、NSURLSessionDownloadTask简单下载

NSURLSessionDownloadTask同样提供了通过NSURL和NSURLRequest两种方式来初始化并通过block进行回调的方法.下面以NSURL初始化为例:

NSURLSession *session = [NSURLSession sharedSession];
NSURL *url = [NSURL URLWithString:@"http://www.daka.com/resources/image/icon.png"] ;
NSURLSessionDownloadTask *task = [session downloadTaskWithURL:url completionHandler:^(NSURL *location, NSURLResponse *response, NSError *error) {
    // location是沙盒中tmp文件夹下的一个临时url,文件下载后会存到这个位置,由于tmp中的文件随时可能被删除,所以我们需要自己需要把下载的文件挪到需要的地方
    NSString *path = [[NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES) lastObject] stringByAppendingPathComponent:response.suggestedFilename];
    [[NSFileManager defaultManager] moveItemAtURL:location toURL:[NSURL fileURLWithPath:path] error:nil];
}];
    // 启动任务
[task resume];
/*
Tips:
需要注意的就是需要将下载到tmp文件夹的文件转移到需要的目录.原因在代码中已经贴出.
response.suggestedFilename是从相应中取出文件在服务器上存储路径的最后部分,如数据在服务器的url为http://www.daka.com/resources/image/icon.png, 那么其suggestedFilename就是icon.png.
*/
3.2.5、NSURLSessionDownloadTask的NSURLSessionDownloadDelegate代理方法
// 每次写入调用(会调用多次)
- (void)URLSession:(NSURLSession *)session downloadTask:(NSURLSessionDownloadTask *)downloadTask didWriteData:(int64_t)bytesWritten totalBytesWritten:(int64_t)totalBytesWritten totalBytesExpectedToWrite:(int64_t)totalBytesExpectedToWrite 
{
      // 可在这里通过已写入的长度和总长度算出下载进度
      CGFloat progress = 1.0 * totalBytesWritten / totalBytesExpectedToWrite; NSLog(@"%f",progress);
}
// 下载完成调用
- (void)URLSession:(NSURLSession *)session downloadTask:(NSURLSessionDownloadTask *)downloadTask didFinishDownloadingToURL:(NSURL *)location 
{ 
      // location还是一个临时路径,需要自己挪到需要的路径(caches下面) NSString *filePath = [[NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES) lastObject] stringByAppendingPathComponent:downloadTask.response.suggestedFilename]; [[NSFileManager defaultManager] moveItemAtURL:location toURL:[NSURL fileURLWithPath:filePath] error:nil];
}
// 任务完成调用
- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didCompleteWithError:(NSError *)error 
{
}
3.2.6、NSURLSessionDownloadTask断点下载
// 使用这种方式取消下载可以得到将来用来恢复的数据,保存起来
[self.task cancelByProducingResumeData:^(NSData *resumeData) {
    self.resumeData = resumeData;
}];
// 由于下载失败导致的下载中断会进入此协议方法,也可以得到用来恢复的数据
- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didCompleteWithError:(NSError *)error
{
    // 保存恢复数据
    self.resumeData = error.userInfo[NSURLSessionDownloadTaskResumeData];
}
// 恢复下载时接过保存的恢复数据
self.task = [self.session downloadTaskWithResumeData:self.resumeData];
// 启动任务
[self.task resume];

//程序强制退出就无法断点下载了,具体方法见http://blog.csdn.net/qianlima210210/article/details/49303703

3.2.7、NSURLSessionUploadTask

在NSURLSession中,文件上传方式主要有以下两种:

//第一种方式
NSURLSessionUploadTask *task =
[[NSURLSession sharedSession] uploadTaskWithRequest:request
                                           fromFile:fileName
                                  completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) {
}];
//第二种方式
[self.session uploadTaskWithRequest:request
                            fromData:body
                   completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) {
 NSLog(@"-------%@", [NSJSONSerialization JSONObjectWithData:data options:kNilOptions error:nil]);
 }];
/*
处于安全性考虑,通常我们会使用POST方式进行文件上传,所以较多使用第二种方式.
但是,NSURLSession并没有为我们提供比NSURLConnection更方便的文件上传方式.方法中body处的参数需要填写request的请求体(http协议规定格式的大长串).

关于NSURLSessionConfiguration的使用可以参考http://www.jianshu.com/p/fafc67475c73
以及http://www.tuicool.com/articles/VBv2qe

由于苹果已经弃用NSURLConnection,所以在此暂时只介绍AFNetworking3.0及3.0+以上的使用方法

4.1、AFNetworking的导入

本人习惯使用cocoaPods 进行管理第三方类库,导入过程不做详细介绍,不会的可以上网查看教程

4.2、简单GET请求

AFHTTPSessionManager *manager = [AFHTTPSessionManager manager]; 
[manager GET:URL parameters:nil progress:^(NSProgress * _Nonnull downloadProgress) {

 } success:^(NSURLSessionDataTask * _Nonnull task, id _Nullable responseObject) {
             NSLog(@"这里打印请求成功要做的事");
 }failure:^(NSURLSessionDataTask * _Nullable task, NSError * _Nonnull error) { 
             NSLog(@"%@",error); //这里打印错误信息
}];

4.3、简单POST请求

AFHTTPSessionManager *manager = [AFHTTPSessionManager manager];
NSMutableDictionary *parameters = @{@"":@"",@"":@""};
[manager POST:URL parameters:parameters progress:^(NSProgress * _Nonnull uploadProgress) {

} success:^(NSURLSessionDataTask * _Nonnull task, id _Nullable responseObject) {

} failure:^(NSURLSessionDataTask * _Nullable task, NSError * _Nonnull error) {

}];

4.4、下载

- (void)downLoad{ 
    //1.创建管理者对象 
    AFHTTPSessionManager *manager = [AFHTTPSessionManager manager]; 
    //2.确定请求的URL地址
    NSURL *url = [NSURL URLWithString:@""];
    //3.创建请求对象 
    NSURLRequest *request = [NSURLRequest requestWithURL:url]; 
    //4.下载任务 
    NSURLSessionDownloadTask *task = [manager downloadTaskWithRequest:request progress:^(NSProgress * _Nonnull downloadProgress) {
           //打印下下载进度 
           NSLog(@"%lf",1.0 * downloadProgress.completedUnitCount / downloadProgress.totalUnitCount);
    } destination:^NSURL * _Nonnull(NSURL * _Nonnull targetPath, NSURLResponse * _Nonnull response) { 
           //下载地址 
           NSLog(@"默认下载地址:%@",targetPath);
           //设置下载路径,通过沙盒获取缓存地址,最后返回NSURL对象 
           NSString *filePath = [NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES)lastObject];
           return [NSURL URLWithString:filePath];
    } completionHandler:^(NSURLResponse * _Nonnull response, NSURL * _Nullable filePath, NSError * _Nullable error) {
           //下载完成调用的方法 WKNSLog(@"下载完成:"); 
           NSLog(@"%@--%@",response,filePath);
    }];
 //开始启动任务 
[task resume];
}

4.5、上传

//第一种方法是通过工程中的文件进行上传
- (void)upLoad1{

    //1。创建管理者对象
    AFHTTPSessionManager *manager = [AFHTTPSessionManager manager];

    //2.上传文件
    NSDictionary *dict = @{@"username":@"1234"};

    NSString *urlString = @"22222";
    [manager POST:urlString parameters:dict constructingBodyWithBlock:^(id<AFMultipartFormData>  _Nonnull formData) {
        //上传文件参数
        UIImage *iamge = [UIImage imageNamed:@"123.png"];
        NSData *data = UIImagePNGRepresentation(iamge);
        //这个就是参数
        [formData appendPartWithFileData:data name:@"file" fileName:@"123.png" mimeType:@"image/png"];

    } progress:^(NSProgress * _Nonnull uploadProgress) {

        //打印下上传进度
        WKNSLog(@"%lf",1.0 *uploadProgress.completedUnitCount / uploadProgress.totalUnitCount);
    } success:^(NSURLSessionDataTask * _Nonnull task, id  _Nullable responseObject) {

        //请求成功
        WKNSLog(@"请求成功:%@",responseObject);

    } failure:^(NSURLSessionDataTask * _Nullable task, NSError * _Nonnull error) {
        //请求失败
        WKNSLog(@"请求失败:%@",error);
    }];

}

//第二种是通过URL来获取路径,进入沙盒或者系统相册等等
- (void)upLoda2{
    //1.创建管理者对象
    AFHTTPSessionManager *manager = [AFHTTPSessionManager manager];
    //2.上传文件
    NSDictionary *dict = @{@"username":@"1234"};

    NSString *urlString = @"22222";
    [manager POST:urlString parameters:dict constructingBodyWithBlock:^(id<AFMultipartFormData>  _Nonnull formData) {

        [formData appendPartWithFileURL:[NSURL fileURLWithPath:@"文件地址"] name:@"file" fileName:@"1234.png" mimeType:@"application/octet-stream" error:nil];
    } progress:^(NSProgress * _Nonnull uploadProgress) {

        //打印下上传进度
        WKNSLog(@"%lf",1.0 *uploadProgress.completedUnitCount / uploadProgress.totalUnitCount);
    } success:^(NSURLSessionDataTask * _Nonnull task, id  _Nullable responseObject) {

        //请求成功
        WKNSLog(@"请求成功:%@",responseObject);

    } failure:^(NSURLSessionDataTask * _Nullable task, NSError * _Nonnull error) {

        //请求失败
        WKNSLog(@"请求失败:%@",error);
    }];
}

4.6、 网络监听

- (void)AFNetworkStatus{ 
       //1.创建网络监测者
       AFNetworkReachabilityManager *manager = [AFNetworkReachabilityManager sharedManager];
       /*枚举里面四个状态 分别对应 未知 无网络 数据 WiFi 
       typedef NS_ENUM(NSInteger, AFNetworkReachabilityStatus) {          
               AFNetworkReachabilityStatusUnknown = -1, 未知          

               AFNetworkReachabilityStatusNotReachable = 0, 无网络 

               AFNetworkReachabilityStatusReachableViaWWAN = 1, 蜂窝数据网络 

               AFNetworkReachabilityStatusReachableViaWiFi = 2, WiFi
       }; 
       */ 
      [manager setReachabilityStatusChangeBlock:^(AFNetworkReachabilityStatus status) { 
               //这里是监测到网络改变的block 可以写成switch方便 
              //在里面可以随便写事件 
              switch (status)
             { 
                 case AFNetworkReachabilityStatusUnknown: 
                      NSLog(@"未知网络状态"); 
                      break;
                 case AFNetworkReachabilityStatusNotReachable: 
                      NSLog(@"无网络");
                      break; 
                 case AFNetworkReachabilityStatusReachableViaWWAN:             
                      NSLog(@"蜂窝数据网"); 
                      break;
                 case AFNetworkReachabilityStatusReachableViaWiFi: 
                      NSLog(@"WiFi网络");
                      break; 
                 default:
                      break;
            }
      }] ;
}

4.7、 关于请求、返回格式设置

所有的网络请求,均有manager发起

4.7.1、请求格式(requestSerializer)

需要注意的是,默认提交请求的数据是二进制的,返回格式是JSON
如果提交数据是JSON的,需要将请求格式设置为AFJSONRequestSerializer

 //AFHTTPRequestSerializer            二进制格式(默认请求格式)
    manager.requestSerializer = [AFHTTPRequestSerializer serializer];
//AFJSONRequestSerializer            JSON
    manager.requestSerializer = [AFJSONRequestSerializer serializer];
 //AFPropertyListRequestSerializer    PList(是一种特殊的XML,解析起来相对容易)
    manager.requestSerializer = [AFPropertyListRequestSerializer serializer];
4.7.2、 返回格式
 //AFHTTPResponseSerializer           二进制格式
   manager.responseSerializer = [AFHTTPResponseSerializer serializer];
 //AFJSONResponseSerializer           JSON(默认情况下返回json,所以有时后返回的不是json,就要重新设置返回格式)
   manager.responseSerializer = [AFJSONResponseSerializer serializer];
 //AFXMLParserResponseSerializer       XML,只能返回XMLParser,还需要自己通过代理方法解析(下面将介绍用NSXMLParser解析xml)
   manager.responseSerializer = [AFXMLParserResponseSerializer serializer];
 //AFXMLDocumentResponseSerializer (Mac OS X)
 //AFPropertyListResponseSerializer   PList
 //AFImageResponseSerializer          Image
 //AFCompoundResponseSerializer       组合

 //通过acceptableContentTypes可以添加接收的类型,如果没有设置,出错情况下会提示,具体参考http://www.jianshu.com/p/212a128c9a33,可以在AFURLResponseSerialization.m源代码中添加接收的类型
 /*
 - (instancetype)init {
      self = [super init];
      if (!self) {
      return nil;
  }
      self.acceptableContentTypes = [NSSet setWithObjects:@"application/json", @"text/json", @"text/javascript", nil];
      return self; 
 }
*/
 manager.responseSerializer.acceptableContentTypes = [NSSet setWithObject:@"application/xml"];

关于该部分的理解,可以参考http://www.jianshu.com/p/3aa19c12000a

    AFHTTPSessionManager *manager = [AFHTTPSessionManager manager];
    manager.responseSerializer = [AFXMLParserResponseSerializer serializer];
    manager.responseSerializer.acceptableContentTypes = [NSSet setWithObject:@"application/xml"];
    manager.requestSerializer = [AFJSONRequestSerializer serializer];
   //afn默认发起的是异步请求
   [manager GET:@"http://localhost/sources/videos.xml" parameters:nil progress:nil success:^(NSURLSessionDataTask * _Nonnull task, id  _Nullable responseObject) {
        NSLog(@"%@",[NSThread currentThread]);//打印结果是主线程,也就是说,异步请求之后,自动返回主线程
//        NSLog(@"%@",responseObject);
        if ([responseObject isKindOfClass:[NSXMLParser class]]) {
            NSXMLParser *parser = responseObject;
            parser.delegate = self;
            dispatch_async(dispatch_get_global_queue(0, 0), ^{
                [parser parse];
            });
        }

    } failure:^(NSURLSessionDataTask * _Nullable task, NSError * _Nonnull error) {
        NSLog(@"%@",error.localizedDescription);
    }];

-(void)parserDidStartDocument:(NSXMLParser *)parser{
    NSLog(@"打开文档,准备开始解析");
}
-(void)parser:(NSXMLParser *)parser didStartElement:(NSString *)elementName namespaceURI:(NSString *)namespaceURI qualifiedName:(NSString *)qName attributes:(NSDictionary<NSString *,NSString *> *)attributeDict{
    NSLog(@"startElement=%@ attributeDict=%@",elementName,attributeDict);
    if ([elementName isEqualToString:@"video"]) {
        self.currentVideo = [[XFSVideo alloc]init];
        self.currentVideo.videoId = attributeDict[@"videoId"];
    }
}
-(void)parser:(NSXMLParser *)parser foundCharacters:(NSString *)string{
    NSLog(@"foundCharacters=%@",string);
    [self.elementString appendString:string];
}
-(void)parser:(NSXMLParser *)parser didEndElement:(NSString *)elementName namespaceURI:(NSString *)namespaceURI qualifiedName:(NSString *)qName{
    NSLog(@"endelement=%@",elementName);
    NSLog(@"%@",[NSThread currentThread]);
    if ([elementName isEqualToString:@"video"]) {
        [self.videos addObject:self.currentVideo];
        self.currentVideo = nil;
    }else if (![elementName isEqualToString:@"videos"]){
        [self.currentVideo setValue:self.elementString forKeyPath:elementName];
        self.elementString = nil;
    }
}
-(void)parserDidEndDocument:(NSXMLParser *)parser{
    NSLog(@"%@",self.videos);
    NSLog(@"结束文档");
    dispatch_async(dispatch_get_main_queue(), ^{ 
    });
}

在CentOS 7上搭建LNMP环境

From: https://www.mtyun.com/library/18/how-to-install-lnmp-on-centos7/

LNMP是Linux、Nginx、MySQL(MariaDB)和PHP的缩写,这个组合是最常见的WEB服务器的运行环境之一。本文将带领大家在CentOS 7操作系统上搭建一套LNMP环境。

本教程适用于CentOS 7.x版本。

在安装LNMP环境之前,您需要先对CentOS操作系统做一些初始化的工作,可以参考CentOS系统初始化设置


美团云的CentOS系统模板中配置了内网源,下载速度较快,推荐使用yum安装Nginx:

sudo yum install nginx

按照提示,输入yes后开始安装。安装完毕后,Nginx的配置文件在/etc/nginx目录下。使用以下命令启动Nginx:

sudo systemctl start nginx

检查系统中firewalld防火墙服务是否开启,如果已开启,我们需要修改防火墙配置,开启Nginx外网端口访问。

sudo systemctl status firewalld

如果显示active (running),则需要调整防火墙规则的配置。

修改/etc/firewalld/zones/public.xml文件,在zone一节中增加:

<zone>
    ...
    <service name="nginx"/>
<zone>

保存后重新加载firewalld服务:

sudo systemctl reload firewalld

您可以通过浏览器访问 http://<外网IP地址> 来确定Nginx是否已经启动。

最后将Nginx设置为开机启动:

sudo systemctl enable nginx.service

MariaDB是MySQL的一个分支,主要由开源社区进行维护和升级,而MySQL被Oracle收购以后,发展较慢。在CentOS 7的软件仓库中,将MySQL更替为了MariaDB。

我们可以使用yum直接安装MariaDB:

sudo yum install mariadb-server

安装完成之后,执行以下命令重启MariaDB服务:

sudo systemctl start mariadb

MariaDB默认root密码为空,我们需要设置一下,执行脚本:

sudo /usr/bin/mysql_secure_installation

这个脚本会经过一些列的交互问答来进行MariaDB的安全设置。

首先提示输入当前的root密码:

Enter current password for root (enter for none):

初始root密码为空,我们直接敲回车进行下一步。

Set root password? [Y/n]

设置root密码,默认选项为Yes,我们直接回车,提示输入密码,在这里设置您的MariaDB的root账户密码。

Remove anonymous users? [Y/n]

是否移除匿名用户,默认选项为Yes,建议按默认设置,回车继续。

Disallow root login remotely? [Y/n]

是否禁止root用户远程登录?如果您只在本机内访问MariaDB,建议按默认设置,回车继续。 如果您还有其他云主机需要使用root账号访问该数据库,则需要选择n

Remove test database and access to it? [Y/n]

是否删除测试用的数据库和权限? 建议按照默认设置,回车继续。

Reload privilege tables now? [Y/n]

是否重新加载权限表?因为我们上面更新了root的密码,这里需要重新加载,回车。

完成后你会看到Success!的提示,MariaDB的安全设置已经完成。我们可以使用以下命令登录MariaDB:

mysql -uroot -p

按提示输入root密码,就会进入MariaDB的交互界面,说明已经安装成功。

最后我们将MariaDB设置为开机启动。

sudo systemctl enable mariadb

我们可以直接使用yum安装PHP:

sudo yum install php-fpm php-mysql

安装完成后我们将php-fpm启动:

sudo systemctl start php-fpm

将php-fpm设置为开机启动:

sudo systemctl enable php-fpm

php安装完成之后,需要设置一下php session的目录:

sudo mkdir /var/lib/php/session/
sudo chown -R apache:apache /var/lib/php/session/

这时php-fpm已经安装完毕,但是现在需要配置一下Nginx,在/etc/nginx/conf.d目录中新建一个名为php.conf的文件,其内容为:

server {
    listen 8000;
    # pass the PHP scripts to FastCGI server listening on 127.0.0.1:9000
    #
    location ~ \.php$ {
        root           /usr/share/php;
        fastcgi_pass   127.0.0.1:9000;
        fastcgi_index  index.php;
        fastcgi_param  SCRIPT_FILENAME  $document_root$fastcgi_script_name;
        include        fastcgi_params;
    }
}

然后执行以下命令使我们的配置生效:

sudo systemctl reload nginx

以上我们配置了Nginx的8000端口用来测试,如果您在美团云控制台创建机器时选择了绑定防火墙,需要检查该防火墙是否允许8000端口,如果不允许的话,您可以在防火墙设置中新增防火墙,并关联到该主机。

我们在/usr/share/php目录下新建一个名为phpinfo.php的文件用来展示phpinfo信息,文件内容为:

<?php echo phpinfo(); ?>

我们从浏览器打开 http://<外网IP地址>:8000/phpinfo.php,您就能看到phpinfo信息了,说明我们php环境已经部署成功:

验证PHP安装成功后,需要将此phpinfo.php文件删除,线上环境尽量不要暴漏使用的软件版本及路径信息,以防被入侵者利用。


使用美团云内置的yum源,我们可以快速的搭建起LNMP的环境,经过简单的安全设置,就可以达到线上服务部署的要求。

iOS中 strong copy weak assign

自我总结 :
strong :
强应用,指针, 赋值后改变也都会改变
copy :
分配一个新的地址, 如果赋值后改变赋值的内容, 被赋值不会改变 如下述
weak :
弱应用,引用计数不会+1 释放后会被置空
assign 简单变量使用

block 做属性的时候也用copy

1
2
3
typedef void (^Block)(NSString *name);
@property (nonatomic, copy) Block testBlock;

修饰符:

1
2
3
4
· 声明变量的修饰符:__strong, __weak, __unsafe_unretained, __autoreleasing;
· 声明属性的修饰符:strong, weak, unsafe_unretained
· 对象和Core Foundation-style对象直接的转换修饰符号:__bridge,__bridge_retained或CFBridgingRetain, __bridge_transfer或CFBridgingRelease
· 对于线程的安全,有nonatomic,这样效率就更高了,但是不是线程的。如果要线程安全,可以使用atomic,这样在访问是就会有线程锁。

别人的总结

1
2
3
4
5
6
7
8
9
· 所有的属性,都尽可能使用nonatomic,以提高效率,除非真的有必要考虑线程安全。
· NSString:通常都使用copy,以得到新的内存分配,而不只是原来的引用。
· strong:对于继承于NSObject类型的对象,若要声明为强使用,使用strong,若要使用弱引用,使用__weak来引用,用于解决循环强引用的问题。
· weak:对于xib上的控件引用,可以使用weak,也可以使用strong
· __weak:对于变量的声明,如果要使用弱引用,可以使用__weak,如:__weak typeof(Model) weakModel = model;就可以直接使用weakModel了。
· __strong:对于变量的声明,如果要使用强引用,可以使用__strong,默认就是__strong,因此不写与写__strong声明都是一样的。
· unsafe_unretained:这个是比较少用的,几乎没有使用到。在所引用的对象被释放后,该指针就成了野指针,不好控制。
· __unsafe_unretained:也是很少使用。同上。
· __autoreleasing:如果要在循环过程中就释放,可以手动使用__autoreleasing来声明将之放到自动释放池。

Objective-C属性修饰符strong和copy的区别

From: https://segmentfault.com/a/1190000002520583

问题描述

在定义一个类的property时候,为property选择strong还是copy特别注意和研究明白的,如果property是NSString或者NSArray及其子类的时候,最好选择使用copy属性修饰。为什么呢?这是为了防止赋值给它的是可变的数据,如果可变的数据发生了变化,那么该property也会发生变化。

代码示例

还是结合代码来说明这个情况

@interface Person : NSObject
@property (strong, nonatomic) NSArray *bookArray1;
@property (copy, nonatomic) NSArray *bookArray2;
@end

@implementation Person
//省略setter方法
@end

//Person调用
main(){
    NSMutableArray *books = [@[@"book1"] mutableCopy];
    Person *person = [[Person alloc] init];
    person.bookArray1 = books;
    person.bookArray2 = books;
    [books addObject:@"book2"];
    NSLog(@"bookArray1:%@",person.bookArray1);
    NSLog(@"bookArray2:%@",person.bookArray2);
}

我们看到,使用strong修饰的person.bookArray1输出是[book1,book2],而使用copy修饰的person.bookArray2输出是[book1]。这下可以看出来区别了吧。

备注:使用strong,则person.bookArray1与可变数组books指向同一块内存区域,books内容改变,导致person.bookArray1的内容改变,因为两者是同一个东西;而使用copy,person.bookArray2在赋值之前,将books内容复制,创建一个新的内存区域,所以两者不是一回事,books的改变不会导致person.bookArray2的改变。

说到底,其实就是不同的修饰符,对应不同的setter方法,

  1. strong对应的setter方法,是将_property先release(_property release),然后将参数retain(property retain),最后是_property = property。
  2. copy对应的setter方法,是将_property先release(_property release),然后拷贝参数内容(property copy),创建一块新的内存地址,最后_property = property。

一些直播文件格式讲解

From: http://caibaojian.com/toutiao/7318

文章目录
  • 视频格式?编码?
    • 视频编码格式
    • 视频文件格式
  • 直播协议
    • HLS
      • HLS 的弊端
  • RTMP
  • HTTP-FLV
  • FLV 格式浅析
    • FLV Header
    • FLV Packets
  • Media Source Extensions
    • 入门实例
      • MS 对流的解析
  • MediaSource
    • MS 的创建
    • 相关方法
      • addSourceBuffer()
      • removeSourceBuffer()
      • endOfStream()
      • isTypeSupported()
  • MS 的状态
  • MS 属性
  • SourceBuffer
    • 基础内容
    • 事件触发
    • 相关方法

视频格式?编码?

如果我们想要理解 HTML5 视频,首先需要知道,你应该知道,但你不知道的内容?那怎么去判断呢? ok,很简单,我提几个问题即可,如果某些童鞋知道答案的话,可以直接跳过。

  1. 你知道 ogg,mp4,flv,webm(前面加个点 .)这些叫做什么吗?
  2. 那 FLV,MPEG-4,VP8 是啥?
  3. 如果,基友问你要片源,你会说我这是 mp4 的还是 MPEG-4 的呢?

当然,还有一些问题,我这里就不废话了。上面主要想说的其实就两个概念:视频文件格式(容器格式),视频编解码器(视频编码格式)。当然,还有另外一种,叫做音频编解码器。简而言之,就是这三个概念比较重要:

  • 视频文件格式(容器格式)
  • 视频编解码器(视频编码格式)
  • 音频编解码器(音频编码格式)

这里,我们主要讲解一下前面两个。视频一开始会由两个端采集,一个是视频输入口,是一个音频输入口。然后,采集的数据会分别进行相关处理,简而言之就是,将视频/音频流,通过一定的手段转换为比特流。最终,将这里比特流以一定顺序放到一个盒子里进行存放,从而生成我们最终所看到的,比如,mp4/mp3/flv 等等音视频格式。

视频编码格式

视频编码格式就是我们上面提到的第一步,将物理流转换为比特流,并且进行压缩。同样,它的压缩编码格式会决定它的视频文件格式。所以,第一步很重要。针对于 HTML5 中的 video/audio,它实际上是支持多种编码格式的,但局限于各浏览器厂家的普及度,目前视频格式支持度最高的是 MPEG-4/H.264,音频则是 MP3/AC3。(下面就主要说下视频的,音频就先不谈了。)

目前市面上,主流浏览器支持的几个有:

  • H.264
  • MEPG-4 第 2 部分
  • VP8
  • Ogg
  • WebM(免费)

其它格式,我们这里就不过多赘述,来看一下前两个比较有趣的。如下图:

demo

请问,上面箭头所指的编码格式是同一个吗?

答案是:No~

因为,MPEG-4 实际上是于 1999 年提出的一个标准。而 H.264 则是后台作为优化提出的新的标准。简单来说就是,我们通常说的 MPEG-4 其实就是MPEG-4 Part 2。而,H.264 则是MPEG-4(第十部分,也叫ISO/IEC 14496-10),又可以理解为 MPEG-4 AVC。而两者,不同的地方,可以参考:latthias 的讲解。简单的区别是:H.264 压缩率比以前的 MPEG-4(第 2 部分) 高很多。简单可以参考的就是:

demo

详细参考: 编码格式详解

视频文件格式

视频文件格式实际上,我们常常称作为容器格式,也就是,我们一般生活中最经常谈到的格式,flv,mp4,ogg 格式等。它就可以理解为将比特流按照一定顺序放进特定的盒子里。那选用不同格式来装视频有什么问题吗? 答案是,没有任何问题,但是你需要知道如何将该盒子解开,并且能够找到对应的解码器进行解码。那如果按照这样看的话,对于这些 mp4,ogv,webm等等视频格式,只要我有这些对应的解码器以及播放器,那么就没有任何问题。那么针对于,将视频比特流放进一个盒子里面,如果其中某一段出现问题,那么最终生成的文件实际上是不可用的,因为这个盒子本身就是有问题的。 不过,上面有一个误解的地方在于,我只是将视频理解为一个静态的流。试想一下,如果一个视频需要持续不断的播放,例如,直播,现场播报等。这里,我们就拿 TS/PS 流来进行讲解。

  • PS(Program Stream): 静态文件流
  • TS(Transport Stream): 动态文件流

针对于上面两种容器格式,实际上是对一个视频比特流做了不一样的处理。

  • PS: 将完成视频比特流放到一个盒子里,生成固定的文件
  • TS: 将接受到的视频,分成不同的盒子里。最终生成带有多个盒子的文件。

那么结果就是,如果一个或多个盒子出现损坏,PS 格式无法观看,而 TS 只是会出现跳帧或者马赛克效应。两者具体的区别就是:对于视频的容错率越高,则会选用 TS,对视频容错率越低,则会选用 PS。

常用为:

  • AVI:MPEG-2,DIVX,XVID,AC-1,H.264;
  • WMV:WMV,AC-1;
  • RM、RMVB:RV, RM;
  • MOV:MPEG-2,XVID,H.264;
  • TS/PS:MPEG-2,H.264,MPEG-4;
  • MKV:可以封装所有的视频编码格式。

详细参考:视频文件格式

直播协议

2016 年是直播元年,一是由于各大宽带提供商顺应民意增宽降价,二是大量资本流进了直播板块,促进了技术的更新迭代。市面上,最常用的是 Apple 推出的 HLS 直播协议(原始支持 H5 播放),当然,还有 RTMP、HTTP-FLV、RTP等。 这里,再问一个问题:

  1. HLS 和 MPEG-4/H.264 以及容器格式 TS/PS 是啥关系?

简单来说,没关系。

HLS 根本就不会涉及到视频本身的解码问题。它的存在只是为了确保你的视频能够及时,快速,正确的播放。

现在,直播行业依旧很火,而 HTML5 直播,一直以来都是一个比较蛋疼的内容。一是,浏览器厂商更新速度比较慢,二是,这并不是我们前端专攻的一块,所以,有时候的确很鸡肋。当然,进了前端,你就别想着休息。接下来,我们来详细的看一下市面上主流的几个协议。

HLS

HLS 全称是 HTTP Live Streaming。这是 Apple 提出的直播流协议。目前,ios 和 高版本 Android 都支持 HLS。那什么是 HLS 呢? HLS 主要的两块内容是 .m3u8 文件和 .ts 播放文件。接受服务器会将接受到的视频流进行缓存,然后缓存到一定程度后,会将这些视频流进行编码格式化,同时会生成一份 .m3u8 文件和其它很多的 .ts 文件。根据 wiki 阐述,HLS 的基本架构为:
注: 全量列表ts, 播放时候也只魂村部分ts. 具体缓存多少不清楚哪里配置

  • 服务器:后台服务器接受视频流,然后进行编码和片段化。
    • 编码:视频格式编码采用 H.264。音频编码为 AAC, MP3, AC-3,EC-3。然后使用 MPEG-2 Transport Stream 作为容器格式。
    • 分片:将 TS 文件分成若干个相等大小的 .ts 文件。并且生成一个 .m3u8 作为索引文件(确保包的顺序)
  • 分发:由于 HLS 是基于 HTTP 的,所以,作为分发,最常用的就是 CDN 了。
  • 客户端:使用一个 URL 去下载 m3u8 文件,然后,开始下载 ts 文件,下载完成后,使用playback software(即时播放器) 进行播放。

这里,我们着重介绍一下客户端的过程。首先,直播之所以是直播,在于它的内容是实时更新的。那 HLS 是怎么完成呢? 我们使用 HLS 直接就用一个 video 进行包括即可:

<video controls autoplay>  
    <source src="http://devimages.apple.com/iphone/samples/bipbop/bipbopall.m3u8" type="application/vnd.apple.mpegurl" /> 
    <p class="warning">Your browser does not support HTML5 video.</p>  
</video>

根据上面的描述,它实际上就是去请求一个 .m3u8 的索引文件。该文件包含了对 .ts 文件的相关描述,例如:

#EXT-X-VERSION:3            PlayList 的版本,可带可不带。下面有说明
#EXTM3U                     m3u文件头
#EXT-X-TARGETDURATION:10    分片最大时长,单位为 s
#EXT-X-MEDIA-SEQUENCE:1     第一个TS分片的序列号,如果没有,默认为 0
#EXT-X-ALLOW-CACHE          是否允许cache
#EXT-X-ENDLIST              m3u8文件结束符
#EXTINF                     指定每个媒体段(ts)的持续时间(秒),仅对其后面的URI有效

不过,这只是一个非常简单,不涉及任何功能的直播流。实际上,HLS 的整个架构,可以分为:

stream_playlists_2x.png-35.5kB

当然,如果你使用的是 masterplaylist 作为链接,如:

<video controls autoplay>  
    <source src="http://devimages.apple.com/iphone/samples/bipbop/masterplaylist.m3u8" type="application/vnd.apple.mpegurl" /> 
    <p class="warning">Your browser does not support HTML5 video.</p>  
</video>

我们看一下,masterplaylist 里面具体的内容是啥:

#EXTM3U
#EXT-X-VERSION:6
#EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=2855600,CODECS="avc1.4d001f,mp4a.40.2",RESOLUTION=960x540
live/medium.m3u8
#EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=5605600,CODECS="avc1.640028,mp4a.40.2",RESOLUTION=1280x720
live/high.m3u8
#EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=1755600,CODECS="avc1.42001f,mp4a.40.2",RESOLUTION=640x360
live/low.m3u8
#EXT-X-STREAM-INF:PROGRAM-ID=1,BANDWIDTH=545600,CODECS="avc1.42001e,mp4a.40.2",RESOLUTION=416x234
live/cellular.m3u8

EXT-X-STREAM-INF 这个标签头代表:当前用户的播放环境。masterplaylist 主要干的事就是根据, 当前用户的带宽,分辨率,解码器等条件决定使用哪一个流。所以,master playlist 是为了更好的用户体验而存在的。不过,弊端就是后台储备流的量会成倍增加。 现在,我们来主要看一下,如果你使用 master playlist,那么整个流程是啥? 当填写了 master playlist URL,那么用户只会下载一次该 master playlist。接着,播放器根据当前的环境决定使用哪一个 media playlist(就是 子 m3u8 文件)。如果,在播放当中,用户的播放条件发生变化时,播放器也会切换对应的 media playlist。关于 master playlist 内容,我们就先介绍到这里。 关于 HLS,感觉主要内容还在 media playlist 上。当然,media playlist 还分为三种 list:

  • live playlist: 动态列表。顾名思义,该列表是动态变化的,里面的 ts 文件会实时更新,并且过期的 ts 索引会被删除。默认,情况下都是使用动态列表。
  • event playlist: 静态列表。它和动态列表主要区别就是,原来的 ts 文件索引不会被删除,该列表是不断更新,而且文件大小会逐渐增大。它会在文件中,直接添加 #EXT-X-PLAYLIST-TYPE:EVENT 作为标识。
  • VOD playlist: 全量列表。它就是将所有的 ts 文件都列在 list 当中。如果,使用该列表,就和播放一整个视频没有啥区别了。它是使用 #EXT-X-ENDLIST 表示文件结尾。

live playlist DEMO:

#EXTM3U
#EXT-X-VERSION:6
#EXT-X-TARGETDURATION:10
#EXT-X-MEDIA-SEQUENCE:26
#EXTINF:9.901,
http:
#EXTINF:9.901,
http:
#EXTINF:9.501,
http:

evet playlist DEMO:

#EXTM3U
#EXT-X-VERSION:6
#EXT-X-TARGETDURATION:10
#EXT-X-MEDIA-SEQUENCE:0
#EXT-X-PLAYLIST-TYPE:EVENT
#EXTINF:9.9001,
http:
#EXTINF:9.9001,
http:
#EXTINF:9.9001,
http:

VOD playlist DEMO:

#EXTM3U
#EXT-X-VERSION:6
#EXT-X-TARGETDURATION:10
#EXT-X-MEDIA-SEQUENCE:0
#EXT-X-PLAYLIST-TYPE:VOD
#EXTINF:9.9001,
http:
#EXTINF:9.9001,
http:
#EXTINF:9.9001,
http:
#EXT-X-ENDLIST

上面提到过一个 EXT-X-VERSION 这样的标签,这是用来表示当前 HLS 的版本。那 HLS 有哪些版本呢? 根据 apple 官方文档 的说明,我们可以了解到,不同版本的区别:

page.png-18.4kB

当然,HLS 支持的功能,并不只是分片播放(专门适用于直播),它还包括其他应有的功能。

  • 使用 HTTPS 加密 ts 文件
  • 快/倒放
  • 广告插入
  • 不同分辨率视频切换

HLS 的弊端

由于 HLS 是基于 HTTP 的,所以,它关于 HTTP 的好处,我们大部分都了解,比如,高兼容性,高可扩展性等。不过正由于是 HTTP 协议,所以会在握手协议上造成一定的延迟性。HLS 首次连接时,总共的延时包括:

  1. TCP 握手,2. m3u8 文件下载,3. m3u8 下的 ts 文件下载。

其中,每个 ts 文件,大概会存放 5s~10s 的时长,并且每个 m3u8 文件会存放 3~8 个 ts 文件。我们折中算一下,5 个 ts 文件,每个时长大约 8s 那么,总的下来,一共延时 40s。当然,这还不算上 TCP 握手,m3u8 文件下载等问题。那优化办法有吗?有的,那就是减少每个 m3u8 文件中的 ts 数量和 ts 文件时长,不过,这样也会成倍的增加后台承受流量请求的压力。所以,这还是需要到业务中去探索最优的配置(打个广告:腾讯云的直播视频流业务,做的确实挺棒。) 关于 HLS 的详细内容,可以参考:HLS 详解 关于 m3u8 文件的标签内容,可以参考:HLS 标签头详解 总而言之,HLS 之所以能这么流行,关键在于它的支持度是真的广,所以,对于一般 H5 直播来说,应该是非常友好的。不过,既然是直播,关键在于它的实时性,而 HLS 天生就存在一定的延时,所以,就可以考虑其他低延时的方案,比如 RTMP,HTTP-FLV。下面,我们来看一下 RTMP 内容。

RTMP

RTMP 全称为:Real-Time Messaging Protocol 。它是专门应对实时交流场景而开发出来的一个协议。它爹是 Macromedia,后来卖身给了 Adobe。RTMP 根据不同的业务场景,有很多变种:

  • 纯 RTMP 使用 TCP 连接,默认端口为 1935(有可能被封)。
  • RTMPS: 就是 RTMP + TLS/SSL
  • RTMPE: RTMP + encryption。在 RTMP 原始协议上使用,Adobe 自身的加密方法
  • RTMPT: RTMP + HTTP。使用 HTTP 的方式来包裹 RTMP 流,这样能直接通过防火墙。
  • RTMFP: RMPT + UDP。该协议常常用于 P2P 的场景中,针对延时有变态的要求。

既然是 Adobe 公司开发的(算吧),那么,该协议针对的就是 Flash Video,即,FLV。不过,在移动端上,Flash Player 已经被杀绝了,那为啥还会出现这个呢?简单来说,它主要是针对 PC 端的。RTMP 出现的时候,还是 零几 年的时候,IE 还在大行其道,Flash Player 也并未被各大浏览器所排斥。那时候 RTMP 毋庸置疑的可以在视频界有自己的一席之地。

RTMP 由于借由 TCP 长连接协议,所以,客户端向服务端推流这些操作而言,延时性很低。它会将上传的流分成不同的分片,这些分片的大小,有时候变,有时候不会变。默认情况下就是,64B 的音频数据 + 128B 的视频数据 + 其它数据(比如 头,协议标签等)。但 RTMP 具体传输的时候,会将分片进一步划分为包,即,视频包,音频包,协议包等。因为,RTMP 在进行传输的时候,会建立不同的通道,来进行数据的传输,这样对于不同的资源,对不同的通道设置相关的带宽上限。

RTMP 处理的格式是 MP3/ACC + FLV1。 不过,由于支持性的原因,RTMP 并未在 H5 直播中,展示出优势。下列是简单的对比:

dff.png-15.8kB

HTTP-FLV

HTTP-FLV 和 RTMPT 类似,都是针对于 FLV 视频格式做的直播分发流。但,两者有着很大的区别。

  • 相同点
    • 两者都是针对 FLV 格式
    • 两者延时都很低
    • 两者都走的 HTTP 通道
  • 不同点
    • HTTP-FLv
      • 直接发起长连接,下载对应的 FLV 文件
      • 头部信息简单
    • RTMPT
      • 握手协议过于复杂
      • 分包,组包过程耗费精力大

通过上面来看,HTTP-FLV 和 RTMPT 确实不是一回事,但,如果了解 SRS(simple rtmp server),那么 对 HTTP-FLV 应该清楚不少。SRS 本质上,就是 RTMP + FLV 进行传输。因为 RTMP 发的包很容易处理,通常 RTMP 协议会作为视频上传端来处理,然后经由服务器转换为 FLV 文件,通过 HTTP-FLV 下发给用户。

STRU.png-2.9kB

现在市面上,比较常用的就是 HTTP-FLV 进行播放。但,由于手机端上不支持,所以,H5 的 HTTP-FLV 也是一个痛点。不过,现在 [flv.js][24] 可以帮助高版本的浏览器,通过 mediaSource 来进行解析。HTTP-FLV 的使用方式也很简单。和 HLS 一样,只需要添加一个连接即可:

[24]: https://github.com/Bilibili/flv.js?utm_source=tool.lu

不过,并不是末尾是 .flv 的都是 HTTP-FLV 协议,因为,涉及 FLV 的流有三种,它们三种的使用方式都是一模一样的。

  • FLV 文件:相当于就是一整个文件,官方称为 渐进 HTTP 流。它的特点是只能渐进下载,不能进行点播。
  • FLV 伪流:该方式,可以通过在末尾添加 ?start=xxx 的参数,指定返回的对应开始时间视频数据。该方式比上面那种就多了一个点播的功能。本质上还是 FLV 直播。
  • FLV 直播流:这就是 HTTP-FLV 真正所支持的流。SRS 在内部使用的是 RTMP 进行分发,然后在传给用户的使用,经过一层转换,变为 HTTP 流,最终传递给用户。

上面说到,HTTP-FLV 就是长连接,简而言之只需要加上一个 Connection:keep-alive 即可。关键是它的响应头,由于,HTTP-FLV 传递的是视频格式,所有,它的 Content-TypeTransfer-Encoding 需要设置其它值。

Content-Type:video/x-flv
Expires:Fri, 10 Feb 2017 05:24:03 GMT
Pragma:no-cache
Transfer-Encoding:chunked

不过,一般而言,直播服务器一般和业务服务是不会放在一块的,所以这里,可能会额外需要支持跨域直播的相关技术。在 XHR2 里面,解决办法也很简单,直接使用 CORS 即可:

Access-Control-Allow-credentials:true
Access-Control-Allow-max-age:86400
Access-Control-Allow-methods:GET,POST,OPTIONS
Access-Control-Allow-Origin:*
Cache-Control:no-cache
Content-Type:video/x-flv
Expires:Fri, 10 Feb 2017 05:24:03 GMT
Pragma:no-cache
Transfer-Encoding:chunked

对于 HTTP-FLV 来说,关键难点在于 RTMP 和 HTTP 协议的转换,这里我就不多说了。因为,我们主要针对的是前端开发,讲一下和前端相关的内容。

接下来,我们在主要来介绍一下 FLV 格式的。因为,后面我们需要通过 mediaSource 来解码 FLV。

FLV 格式浅析

FLV 原始格式,Adobe 可以直接看 flv格式详解。我这里就抽主要的内容讲讲。FLV 也是与时俱进,以前 FLV 的格式叫做 FLV,新版的可以叫做 F4V。两者的区别,简单的区分方法就是:

  • FLV 是专门针对 Flash 播放器的
  • F4V 是有点像 MEPG 格式的 Flash 播放,主要为了兼容 H.264/ACC。F4V 不支持 FLV(两者本来都不是同一个格式)

这里我们主要针对 FLV 进行相关了解。因为,一般情况下,后台发送视频流时,为了简洁快速,就是发送 FLV 视频。FLV 由于年限比较久,它所支持的内容是 H.263,VP6 codec。FLV 一般可以嵌套在 .swf 文件当中,不过,对于 HTTP-FLV 等 FLV 直播流来说,一般直接使用.flv 文件即可。在 07 年的时候,提出了 F4V 这个视频格式,当然,FLV 等也会向前兼容。

flv

这里,我们来正式介绍一下 FLV 的格式。一个完整的 FLV 流包括 FLV Header + FLV Packets。

FLV Header

FLV 格式头不难,就几个字段:

|Field|Data Type|Default|Details| |:—|:—|:—| |Signature|byte3|“FLV”|有三个B的大小,算是一种身份的象征| |Version|uint8|1|只有 0x01 是有效的。其实就是默认值| |Flags|uint8 bitmask|0x05|表示该流的特征。0x04 是 audio,0x01 是 video,0x05 是 audio+video| |Header Size|uint32_be|9|用来跳过多余的头|

FLV Packets

在 FLV 的头部之后,就正式开始发送 FLV 文件。文件会被拆解为数个包(FLV tags)进行传输。每个包都带有 15B 的头。前 4 个字节是用来代表前一个包的头部内容,用来完成倒放的功能。整个包的结构为:

FLV

具体解释如下:

字段字段大小默认值详解

Size of previous packet
uint32_be
0
关于前一个包的信息,如果是第一个包,则该部分为 NULL

Packet Type
uint8
18
设置包的内容,如果是第一个包,则该部分为 AMF 元数据

Payload Size
uint24_be
varies
该包的大小

Timestamp Lower
uint24_be
0
起始时间戳

Timestamp Upper
uint8
0
持续时间戳,通常加上 Lower 实际上戳,代表整个时间。

Stream ID
uint24_be
0
流的类型,第一个流设为 NULL

Payload Data
freeform
varies
传输数据

其中,由于 Packet Type 的值可以取多个, 需要额外说明一下。

  • Packet Type
    • 1: RTMP 包的大小
    • 3: RTMP 字节读包反馈,RTMP ping,RTMP 服务器带宽,RTMP 客户端带宽
    • 8: 音频和视频的数据
    • 15: RTMP flex 流
    • 24: 经过封装的 flash video。

上面是关于 FLV 简单的介绍。不过,如果没有 Media Source Extensions 的帮助,那么上面说的基本上全是废话。由于,Flash Player 已经被时代所遗弃,所以,我们不能在浏览器上,顺利的播放 FLV 视频。接下来,我们先来详细了解一下 MSE 的相关内容。

Media Source Extensions

在没有 MSE 出现之前,前端对 video 的操作,仅仅局限在对视频文件的操作,而并不能对视频流做任何相关的操作。现在 MSE 提供了一系列的接口,使开发者可以直接提供 media stream。

那 MSE 是如何完成视频流的加载和播放呢?

入门实例

这可以参考 google 的 [MSE 简介][32]

[32]: https://developers.google.com/web/fundamentals/getting-started/primers/media-source-extensions

var vidElement = document.querySelector(‘video’);

if (window.MediaSource) {
  var mediaSource = new MediaSource();
  vidElement.src = URL.createObjectURL(mediaSource);
  mediaSource.addEventListener('sourceopen', sourceOpen);
} else {
  console.log("The Media Source Extensions API is not supported.")
}

function sourceOpen(e) {
  URL.revokeObjectURL(vidElement.src);
  var mime = 'video/webm; codecs="opus, vp9"';
  var mediaSource = e.target;
  var sourceBuffer = mediaSource.addSourceBuffer(mime);
  var videoUrl = 'droid.webm';
  fetch(videoUrl)
    .then(function(response) {
      return response.arrayBuffer();
    })
    .then(function(arrayBuffer) {
      sourceBuffer.addEventListener('updateend', function(e) {
        if (!sourceBuffer.updating && mediaSource.readyState === 'open') {
          mediaSource.endOfStream();
        }
      });
      sourceBuffer.appendBuffer(arrayBuffer);
    });
}

可以从上面的代码看出,一套完整的执行代码,不仅需要使用 MSE 而且,还有一下这些相关的 API。

  • HTMLVideoElement.getVideoPlaybackQuality()
  • SourceBuffer
  • SourceBufferList
  • TextTrack.sourceBuffer
  • TrackDefault
  • TrackDefaultList
  • URL.createObjectURL()
  • VideoPlaybackQuality
  • VideoTrack.sourceBuffer

我们简单讲解一下上面的流程。根据 google 的阐述,整个过程可以为:

image.png-16kB

  • 第一步,通过异步拉取数据。
  • 第二步,通过 MediaSource 处理数据。
  • 第三步,将数据流交给 audio/video 标签进行播放。

而中间传递的数据都是通过 Buffer 的形式来进行传递的。

image.png-29.5kB

中间有个需要注意的点,MS 的实例通过 URL.createObjectURL() 创建的 url 并不会同步连接到 video.src。换句话说,URL.createObjectURL() 只是将底层的流(MS)和 video.src 连接中间者,一旦两者连接到一起之后,该对象就没用了。

那么什么时候 MS 才会和 video.src 连接到一起呢?

创建实例都是同步的,但是底层流和 video.src 的连接时异步的。MS 提供了一个 sourceopen事件给我们进行这项异步处理。一旦连接到一起之后,该 URL object 就没用了,处于内存节省的目的,可以使用 URL.revokeObjectURL(vidElement.src) 销毁指定的 URL object。

mediaSource.addEventListener('sourceopen', sourceOpen);

function sourceOpen(){
    URL.revokeObjectURL(vidElement.src)
}

MS 对流的解析

MS 提供了我们对底层音视频流的处理,那一开始我们怎么决定以何种格式进行编解码呢?

这里,可以使用 addSourceBuffer(mime) 来设置相关的编码器:

var mime = 'video/webm; codecs="opus, vp9"';  
var sourceBuffer = mediaSource.addSourceBuffer(mime);  

然后通过,异步拉取相关的音视频流:

fetch(url)
.then(res=>{
    return res.arrayBuffer();
})
.then(buffer=>{
    sourceBuffer.appendBuffer(buffer);
})

如果视频已经传完了,而相关的 Buffer 还在占用内存,这时候,就需要我们显示的中断当前的 Buffer 内容。那么最终我们的异步处理结果变为:

fetch(url)
.then(res=>{
    return res.arrayBuffer();
})
.then(function(arrayBuffer) {
      sourceBuffer.addEventListener('updateend', function(e) {

        if (!sourceBuffer.updating && mediaSource.readyState === 'open') {

          mediaSource.endOfStream();
        }
      });
      sourceBuffer.appendBuffer(arrayBuffer);
    });

上面我们大致了解了一下关于 Media Source Extensions 的大致流程,但里面的细节我们还没有细讲。接下来,我们来具体看一下 MSE 一篮子的生态技术包含哪些内容。首先是,MediaSource

MediaSource

MS(MediaSource) 可以理解为多个视频流的管理工具。以前,我们只能下载一个清晰度的流,并且不能平滑切换低画质或者高画质的流,而现在我们可以利用 MS 实现这里特性。我们先来简单了解一下他的 API。

MS 的创建

创建一个 MS:

var mediaSource = new MediaSource();

相关方法

addSourceBuffer()

该是用来返回一个具体的视频流,接受一个 mimeType 表示该流的编码格式。例如:

var mimeType = 'video/mp4; codecs="avc1.42E01E, mp4a.40.2"';
var sourceBuffer = mediaSource.addSourceBuffer(mimeType);

sourceBuffer 是直接和视频流有交集的 API。例如:

function sourceOpen (_) {
  var mediaSource = this;
  var sourceBuffer = mediaSource.addSourceBuffer(mimeCodec);
  fetchAB(assetURL, function (buf) {
    sourceBuffer.addEventListener('updateend', function (_) {
      mediaSource.endOfStream();
      video.play();
    });

    sourceBuffer.appendBuffer(buf);
  });
};

它通过 appendBuffer 直接添加视频流,实现播放。不过,在使用 addSourceBuffer 创建之前,还需要保证当前浏览器是否支持该编码格式。

removeSourceBuffer()

用来移除某个 sourceBuffer。移除也主要是考虑性能原因,将不需要的流移除以节省相应的空间,格式为:

mediaSource.removeSourceBuffer(sourceBuffer);

endOfStream()

用来表示接受的视频流的停止,注意,这里并不是断开,相当于只是下好了一部分视频,然后你可以进行播放。此时,MS 的状态变为:ended。例如:

var mediaSource = this;
var sourceBuffer = mediaSource.addSourceBuffer(mimeCodec);
fetchAB(assetURL, function (buf) {
  sourceBuffer.addEventListener('updateend', function (_) {
    mediaSource.endOfStream(); 
    video.play(); 
  });
  sourceBuffer.appendBuffer(buf);
});

isTypeSupported()

该是用来检测当前浏览器是否支持指定视频格式的解码。格式为:

var isItSupported = mediaSource.isTypeSupported(mimeType); 

mimeType 可以为 type 或者 type + codec。

例如:

MediaSource.isTypeSupported('audio/mp3'); 
MediaSource.isTypeSupported('video/mp4'); 
MediaSource.isTypeSupported('video/mp4; codecs="avc1.4D4028, mp4a.40.2"'); 

这里有一份具体的 mimeType 参考列表。

MS 的状态

当 MS 从创建开始,都会自带一个 readyState 属性,用来表示其当前打开的状态。MS 有三个状态:

  • closed: 当前 MS 没有和 media element(比如:video.src) 相关联。创建时,MS 就是该状态。
  • open: source 打开,并且准备接受通过 sourceBuffer.appendBuffer 添加的数据。
  • ended: 当 endOfStream() 执行完成,会变为该状态,此时,source 依然和 media element 连接。

    var mediaSource = new MediaSource;
    mediaSource.readyState;

当由 closed 变为 open 状态时,需要监听 sourceopen 事件。

video.src = URL.createObjectURL(mediaSource);
mediaSource.addEventListener('sourceopen', sourceOpen);

MS 针对这几个状态变化,提供了相关的事件:sourceopensourceendedsourceclose

  • sourceopen: 当 “closed” to “open” 或者 “ended” to “open” 时触发。
  • sourceended: 当 “open” to “ended” 时触发。
  • sourceclose: 当 “open” to “closed” 或者 “ended” to “closed” 时触发。

MS 还提供了其他的监听事件 sourceopen,sourceended,sourceclose,updatestart,update,updateend,error,abort,addsourcebuffer,removesourcebuffer. 这里主要选了比较重要的,其他的可以参考官方文档。

MS 属性

比较常用的属性有: duration,readyState。

  • duration: 获得当前媒体播放的时间,既可以设置(get),也可以获取(set)。单位为 s(秒)

    mediaSource.duration = 5.5;
    var myDuration = mediaSource.duration;

在实际应用中为:

sourceBuffer.addEventListener('updateend', function (_) {
      mediaSource.endOfStream();
      mediaSource.duration = 120; 
      video.play();
    });
  • readyState: 获得当前 MS 的状态。取值上面已经讲过了: closedopenended

    var mediaSource = new MediaSource;

以及:

sourceBuffer.addEventListener('updateend', function (_) {
      mediaSource.endOfStream(); 
      video.play();
    });

除了上面两个属性外,还有 sourceBuffersactiveSourceBuffers 这两个属性。用来返回通过 addSourceBuffer() 创建的 SourceBuffer 数组。这没啥过多的难度。

接下来我们就来看一下靠底层的 sourceBuffer

SourceBuffer

SourceBuffer 是由 mediaSource 创建,并直接和 HTMLMediaElement 接触。简单来说,它就是一个流的容器,里面提供的 append()remove() 来进行流的操作,它可以包含一个或者多个 media segments。同样,接下来,我们再来看一下该构造函数上的基本属性和内容。

基础内容

前面说过 sourceBuffer 主要是一个用来存放流的容器,那么,它是怎么存放的,它存放的内容是啥,有没有顺序等等。这些都是 sourceBuffer 最最根本的问题。OK,接下来,我们来看一下的它的基本架构有些啥。

参考 [W3C][36],可以基本了解到里面的内容为:

[36]: https://www.w3.org/TR/media-source/#sourcebuffer

interface SourceBuffer : EventTarget {
attribute AppendMode mode;
readonly attribute boolean updating;
readonly attribute TimeRanges buffered;
attribute double timestampOffset;
readonly attribute AudioTrackList audioTracks;
readonly attribute VideoTrackList videoTracks;
readonly attribute TextTrackList textTracks;
attribute double appendWindowStart;
attribute unrestricted double appendWindowEnd;
attribute EventHandler onupdatestart;
attribute EventHandler onupdate;
attribute EventHandler onupdateend;
attribute EventHandler onerror;
attribute EventHandler onabort;
void appendBuffer(BufferSource data);
void abort();
void remove(double start, unrestricted double end);
};

上面这些属性决定了其 sourceBuffer 整个基础。

首先是 mode。上面说过,SB(SourceBuffer) 里面存储的是 media segments(就是你每次通过 append 添加进去的流片段)。SB.mode 有两种格式:

  • segments: 乱序排放。通过 timestamps 来标识其具体播放的顺序。比如:20s的 buffer,30s 的 buffer 等。
  • sequence: 按序排放。通过 appendBuffer 的顺序来决定每个 mode 添加的顺序。timestamps 根据 sequence 自动产生。

那么上面两个哪个是默认值呢?

看情况,讲真,没骗你。

media segments 天生自带 timestamps,那么 mode 就为 segments ,否则为sequence。所以,一般情况下,我们是不用管它的值。不过,你可以在后面,将 segments设置为 sequence 这个是没毛病的。反之,将 sequence 设置为 segments 就有问题了。

var bufferMode = sourceBuffer.mode;
if (bufferMode == 'segments') {
  sourceBuffer.mode = 'sequence';
}

然后另外两个就是 bufferedupdating

  • buffered:返回一个 timeRange 对象。用来表示当前被存储在 SB 中的 buffer。
  • updating: 返回 Boolean,表示当前 SB 是否正在被更新。例如: SourceBuffer.appendBuffer(), SourceBuffer.appendStream(), SourceBuffer.remove() 调用时。

另外还有一些其他的相关属性,比如 textTracks,timestampOffset,trackDefaults,这里就不多说了。实际上,SB 是一个事件驱动的对象,一些常见的处理,都是在具体的事件中完成的。那么它又有哪些事件呢?

事件触发

在 SB 中,相关事件触发包括:

  • updatestart: 当 updating 由 false 变为 true。
  • update:当 append()/remove() 方法被成功调用完成时,updating 由 true 变为 false。
  • updateend: append()/remove() 已经结束
  • error: 在 append() 过程中发生错误,updating 由 true 变为 false。
  • abort: 当 append()/remove() 过程中,使用 abort() 方法废弃时,会触发。此时,updating 由 true 变为 false。

注意上面有两个事件比较类似:updateupdateend。都是表示处理的结束,不同的是,update 比 updateend 先触发。

sourceBuffer.addEventListener('updateend', function (e) {

      mediaSource.endOfStream();
      video.play();
    });

相关方法

SB 处理流的方法就是 +/- : appendBuffer, remove。另外还有一个中断处理函数 abort()

  • appendBuffer(ArrayBuffer):用来添加 ArrayBuffer。该 ArrayBuffer 一般是通过 fetch 的response.arrayBuffer(); 来获取的。
  • remove(start, end): 用来移除具体某段的 media segments。
    • @param start/end: 都是时间单位(s)。用来表示具体某段的 media segments 的范围。
  • abort(): 用来放弃当前 append 流的操作。不过,该方法的业务场景也比较有限。它只能用在当 SB 正在更新流的时候。即,此时通过 fetch,已经接受到新流,并且使用 appendBuffer 添加,此为开始的时间。然后到 updateend 事件触发之前,这段时间之内调用 abort()。有一个业务场景是,当用户移动进度条,而,此时 fetch 已经获取前一次的 media segments,那么可以使用 abort 放弃该操作,转而请求新的 media segments。具体可以参考:abort 使用

上面主要介绍了处理音视频流需要用的 web 技术,后面章节,我们接入实战,具体来讲一下,如何做到使用 MSE 进行 remux 和 demux。

原文链接: https://www.villianhr.com/2017/03/31/全面进阶 H5 直播

IOS 错误日志分析

一直以来对ios 的错误日志都是朦朦胧胧的也没有太多的时间去增强代码的健壮性, 今天搞了下,记录下也为后人做贡献

第一步:打开 Xcode,选择”Window——>Organizer”

第二步:选择对应版本的 Archive 包,”右键——>Show in Finder”

第三步:选择对应版本的”.xcarchive”文件,”右键——>显示包内容”

dSYMs 文件夹下或许就有.dSYM
比如我的
NotificationService.appex.dSYM
这个是ios10 推送的,发现没有主应用的
找答案发现需要设置编译参数

Debug Information Format

Release 设置成DWARF with dSYM File
如何

然后是解析部分,

先转一个比较好的博客
http://lieyunye.github.io/blog/2013/09/10/how-to-analyse-ios-crash-log/