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/

swift 可选协议

可选接口和接口扩展
转自 : http://swifter.tips/objc-protocol/

Objective-C 中的 protocol 里存在 @optional 关键字,被这个关键字修饰的方法并非必须要被实现。我们可以通过接口定义一系列方法,然后由实现接口的类选择性地实现其中几个方法。在 Cocoa API 中很多情况下接口方法都是可选的,这点和 Swift 中的 protocol 的所有方法都必须被实现这一特性完全不同。

那些如果没有实现则接口就无法正常工作的方法一般是必须的,而相对地像作为事件通知或者对非关键属性进行配置的方法一般都是可选的。最好的例子我想应该是 UITableViewDataSource 和 UITableViewDelegate。前者中有两个必要方法:

1
2
--tableView:numberOfRowsInSection:
--tableView:cellForRowAtIndexPath:

分别用来计算和准备 tableView 的高度以及提供每一个 cell 的样式,而其他的像是返回 section 个数或者询问 cell 是否能被编辑的方法都有默认的行为,都是可选方法;后者 (UITableViewDelegate) 中的所有方法都是详细的配置和事件回传,因此全部都是可选的。

原生的 Swift protocol 里没有可选项,所有定义的方法都是必须实现的。如果我们想要像 Objective-C 里那样定义可选的接口方法,就需要将接口本身定义为 Objective-C 的,也即在 protocol 定义之前加上 @objc。另外和 Objective-C 中的 @optional 不同,我们使用没有 @ 符号的关键字 optional 来定义可选方法:

1
2
3
@objc protocol OptionalProtocol {
optional func optionalMethod()
}

另外,对于所有的声明,它们的前缀修饰是完全分开的。也就是说你不能像是在 Objective-C 里那样用一个 @optional 指定接下来的若干个方法都是可选的了,必须对每一个可选方法添加前缀,对于没有前缀的方法来说,它们是默认必须实现的:

1
2
3
4
5
@objc protocol OptionalProtocol {
optional func optionalMethod() // 可选
func necessaryMethod() // 必须
optional func anotherOptionalMethod() // 可选
}

一个不可避免的限制是,使用 @objc 修饰的 protocol 就只能被 class 实现了,也就是说,对于 struct 和 enum 类型,我们是无法令它们所实现的接口中含有可选方法或者属性的。另外,实现它的 class 中的方法还必须也被标注为 @objc,或者整个类就是继承自 NSObject。这对我们写代码来说是一种很让人郁闷的限制。

在 Swift 2.0 中,我们有了另一种选择,那就是使用 protocol extension。我们可以在声明一个 protocol 之后再用 extension 的方式给出部分方法默认的实现。这样这些方法在实际的类中就是可选实现的了。还是举上面的例子,使用接口扩展的话,会是这个样子:

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
protocol OptionalProtocol {
func optionalMethod() // 可选
func necessaryMethod() // 必须
func anotherOptionalMethod() // 可选
}
extension OptionalProtocol {
// 把需要可选的在这里做一个空实现
func optionalMethod() {
print("Implemented in extension")
}
func anotherOptionalMethod() {
print("Implemented in extension")
}
}
class MyClass: OptionalProtocol {
func necessaryMethod() {
print("Implemented in Class3")
}
func optionalMethod() {
print("Implemented in Class3")
}
}
let obj = MyClass()
obj.necessaryMethod() // Implemented in Class3
obj.optionalMethod() // Implemented in Class3
obj.anotherOptionalMethod() // Implemented in extension

protocol extension

转自: http://swifter.tips/protocol-extension/

Swift 2 中引入了一个非常重要的特性,那就是 protocol extension。在 Swift 1.x 中,extension 仅只能作用在实际的类型上 (也就是 class, struct 等等),而不能扩展一个 protocol。在 Swift 中,标准库的功能基本都是基于 protocol 来实现的,举个最简单的例子,我们每天使用的 Array 就是遵守了 CollectionType 这个 protocol 的。CollectionType 可以说是 Swift 中非常重要的接口,除了 Array 以外,像是 Dictionary 和 Set 也都实现了这个接口所定义的内容。

在 protocol 不能被扩展的时候,当我们想要为实现了某个接口的所有类型添加一些另外的共通的功能时,会非常麻烦。一个很好的例子是 Swift 1.x 时像是 map 或者 filter 这样的函数。大体来说,我们有两种思路进行添加:第一种方式是在接口中定义这个方法,然后在所有实现了这个接口的类型中都去实现一遍。每有一个这样的类型,我们就需要写一份类似甚至相同的方法,这显然是不可取的,不仅麻烦,而且完全没有可维护性。另一种方法是在全局范围实现一个接受该 protocol 的实例的方法,相比于前一种方式,我们只需要维护一份代码,显然要好不少,但是缺点在于在全局作用域中引入了只和特定 protocol 有关的东西,这并不符合代码设计的美学。作为妥协,Apple 在 Swift 1.x 中采用的是后一种,也就是全局方法,如果你尝试寻找的话,可以在 Swift 1.x 的标准库的全局 scope 中找到像是 map 和 filter 这样的方法。

在 Swift 2 中这个问题被彻底解决了。现在我们可以对一个已有的 protocol 进行扩展,而扩展中实现的方法将作为实现扩展的类型的默认实现。也就是说,假设我们有下面的 protocol 声明,以及一个对该接口的扩展:

1
2
3
4
5
6
7
8
9
protocol MyProtocol {
func method()
}
extension MyProtocol {
func method() {
print("Called")
}
}

在具体的实现这个接口的类型中,即使我们什么都不写,也可以编译通过。进行调用的话,会直接使用 extension 中的实现:

1
2
3
4
5
6
7
struct MyStruct: MyProtocol {
}
MyStruct().method()
// 输出:
// Called in extension

当然,如果我们需要在类型中进行其他实现的话,可以像以前那样在具体类型中添加这个方法:

1
2
3
4
5
6
7
8
9
struct MyStruct: MyProtocol {
func method() {
print("Called in struct")
}
}
MyStruct().method()
// 输出:
// Called in struct

也就是说,protocol extension 为 protocol 中定义的方法提供了一个默认的实现。有了这个特性以后,之前被放在全局环境中的接受 CollectionType 的 map 方法,就可以被移动到 CollectionType 的接口扩展中去了:

1
2
3
4
extension CollectionType {
public func map<T>(@noescape transform: (Self.Generator.Element) -> T) -> [T]
//...
}

在日常开发中,另一个可以用到 protocol extension 的地方是 optional 的接口方法。通过提供 protocol 的 extension,我们为 protocol 提供了默认实现,这相当于变相将 protocol 中的方法设定为了 optional。关于这个,我们在可选接口和接口扩展一节中已经讲述过,就不再重复了。

对于 protocol extension 来说,有一种会非常让人迷惑的情况,就是在接口的扩展中实现了接口里没有定义的方法时的情况。举个例子,比如我们定义了这样的一个接口和它的一个扩展:

1
2
3
4
5
6
7
8
9
protocol A1 {
func method1() -> String
}
struct B1: A1 {
func method1() -> String {
return "hello"
}
}

在使用的时候,无论我们将实例的类型为 A1 还是 B1,因为实现只有一个,所以没有任何疑问,调用方法时的输出都是 “hello”:

1
2
3
4
5
6
7
8
let b1 = B1() // b1 is B1
b1.method1()
// hello
let a1: A1 = B1()
// a1 is A1
a1.method1()
// hello

但是如果在接口里只定义了一个方法,而在接口扩展中实现了额外的方法的话,事情就变得有趣起来了。考虑下面这组接口和它的扩展:

1
2
3
4
5
6
7
8
9
10
11
12
13
protocol A2 {
func method1() -> String
}
extension A2 {
func method1() -> String {
return "hi"
}
func method2() -> String {
return "hi"
}
}

扩展中除了实现接口定义的 method1 之外,还定义了一个接口中不存在的方法 method2。我们尝试来实现这个接口:

1
2
3
4
5
6
7
8
9
struct B2: A2 {
func method1() -> String {
return "hello"
}
func method2() -> String {
return "hello"
}
}

B2 中实现了 method1 和 method2。接下来,我们尝试初始化一个 B2 对象,然后对这两个方法进行调用:

1
2
3
4
let b2 = B2()
b2.method1() // hello
b2.method2() // hello

结果在我们的意料之中,虽然在 protocol extension 中已经实现了这两个方法,但是它们只是默认的实现,我们在具体实现接口的类型中可以对默认实现进行覆盖,这非常合理。但是如果我们稍作改变,在上面的代码后面继续添加:

1
2
3
4
let a2 = b2 as A2
a2.method1() // hello
a2.method2() // hi

a2 和 b2 是同一个对象,只不过我们通过 as 告诉编译器我们在这里需要的类型是 A2。但是这时候在这个同样的对象上调用同样的方法调用却得到了不同的结果,发生了什么?

我们可以看到,对 a2 调用 method2 实际上是接口扩展中的方法被调用了,而不是 a2 实例中的方法被调用。我们不妨这样来理解:对于 method1,因为它在 protocol 中被定义了,因此对于一个被声明为遵守接口的类型的实例 (也就是对于 a2) 来说,可以确定实例必然实现了 method1,我们可以放心大胆地用动态派发的方式使用最终的实现 (不论它是在类型中的具体实现,还是在接口扩展中的默认实现);但是对于 method2 来说,我们只是在接口扩展中进行了定义,没有任何规定说它必须在最终的类型中被实现。在使用时,因为 a2 只是一个符合 A2 接口的实例,编译器对 method2 唯一能确定的只是在接口扩展中有一个默认实现,因此在调用时,无法确定安全,也就不会去进行动态派发,而是转而编译期间就确定的默认实现。

也许在这个例子中你会觉得无所谓,因为实际中估计并不会有人将一个已知类型实例转回接口类型。但是要考虑到如果你的一些泛型 API 中有类似的直接拿到一个接口类型的结果的时候,调用它的扩展方法时就需要特别小心了:一般来说,如果有这样的需求的话,我们可以考虑将这个接口类型再转回实际的类型,然后进行调用。

整理一下相关的规则的话:

如果类型推断得到的是实际的类型
那么类型中的实现将被调用;如果类型中没有实现的话,那么接口扩展中的默认实现将被使用
如果类型推断得到的是接口,而不是实际类型
并且方法在接口中进行了定义,那么类型中的实现将被调用;如果类型中没有实现,那么接口扩展中的默认实现被使用
否则 (也就是方法没有在接口中定义),扩展中的默认实现将被调用

UIView实现水平翻转

1
2
let view = UIView(frame: CGRect(x: 100, y: 100, width: 100, height: 100))
view.layer.transform = CATransform3DMakeRotation(CGFloat(M_PI), 0, 1, 0)

swift LOG 输出

LOG 输出,一直用oc 的条件编译, 刚发现swift 同样可以实现
现记录
转: http://swifter.tips/log/
http://swifter.tips/condition-compile/

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
LOG 输出
由 王巍 (@ONEVCAT) 发布于 2015-12-30
Log 输出是程序开发中很重要的组成部分,虽然它并不是直接的业务代码,但是却可以忠实地反映我们的程序是如何工作的,以及记录程序运行的过程中发生了什么。
Swift 中,最简单的输出方法就是使用 print,在我们关心的地方输出字符串和值。但是这并不够,试想一下当程序变得非常复杂的时候,我们可能会输出很多内容,而想在其中寻找到我们希望的输出其实并不容易。我们往往需要更好更精确的输出,这包括输出这个 log 的文件,调用的行号以及所处的方法名字等等。
我们当然可以在 print 的时候将当前的文件名字和那些必要的信息作为参数同我们的消息一起进行打印:
// Test.swift
func method() {
//...
print("文件名:Test.swift, 方法名:method,这是一条输出")
//...
}
但是这显然非常麻烦,每次输入文件名和方法名不说,随着代码的改变,这些 Log 的位置也可能发生改变,这时我们可能还需要不断地去维护这些输出,代价实在太大。
Swift 中,编译器为我们准备了几个很有用的编译符号,用来处理类似这样的需求,它们分别是:
符号 类型 描述
#file String 包含这个符号的文件的路径
#line Int 符号出现处的行号
#column Int 符号出现处的列
#function String 包含这个符号的方法名字
因此,我们可以通过使用这些符号来写一个好一些的 Log 输出方法:
func printLog<T>(message: T,
file: String = #file,
method: String = #function,
line: Int = #line)
{
print("\((file as NSString).lastPathComponent)[\(line)], \(method): \(message)")
}
这样,在进行 log 的时候我们只需要使用这个方法就能完成文件名,行号以及方法名的输出了。最棒的是,我们不再需要对这样的输出进行维护,无论在哪里它都能正确地输出各个参数:
// Test.swift
func method() {
//...
printLog("这是一条输出")
//...
}
// 输出:
// Test.swift[62], method(): 这是一条输出
另外,对于 log 输出更多地其实是用在程序开发和调试的过程中的,过多的输出有可能对运行的性能造成影响。在 Release 版本中关闭掉向控制台的输出也是软件开发中一种常见的做法。如果我们在开发中就注意使用了统一的 log 输出的话,这就变得非常简单了。使用条件编译的方法,我们可以添加条件,并设置合适的编译配置,使 printLog 的内容在 Release 时被去掉,从而成为一个空方法:
func printLog<T>(message: T,
file: String = #file,
method: String = #function,
line: Int = #line)
{
#if DEBUG
print("\((file as NSString).lastPathComponent)[\(line)], \(method): \(message)")
#endif
}
新版本的 LLVM 编译器在遇到这个空方法时,甚至会直接将这个方法整个去掉,完全不去调用它,从而实现零成本。

主要用到条件编译
上述需要设置

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
条件编译
由 王巍 (@ONEVCAT) 发布于 2014-12-02
在 C 系语言中,可以使用 #if 或者 #ifdef 之类的编译条件分支来控制哪些代码需要编译,而哪些代码不需要。Swift 中没有宏定义的概念,因此我们不能使用 #ifdef 的方法来检查某个符号是否经过宏定义。但是为了控制编译流程和内容,Swift 还是为我们提供了几种简单的机制来根据需求定制编译内容的。
首先是 #if 这一套编译标记还是存在的,使用的语法也和原来没有区别:
#if <condition>
#elseif <condition>
#else
#endif
当然,#elseif#else 是可选的。
但是这几个表达式里的 condition 并不是任意的。Swift 内建了几种平台和架构的组合,来帮助我们为不同的平台编译不同的代码,具体地:
方法 可选参数
os() OSX, iOS
arch() x86_64, arm, arm64, i386
注意这些方法和参数都是大小写敏感的。举个例子,如果我们统一我们在 iOS 平台和 Mac 平台的关于颜色的 API 的话,一种可能的方法就是配合 typealias 进行条件编译:
#if os(OSX)
typealias Color = NSColor
#else
typealias Color = UIColor
#endif
另外对于 arch() 的参数需要说明的是 arm 和 arm64 两项分别对应 32 位 CPU 和 64 位 CPU 的真机情况,而对于模拟器,相应地 32 位设备的模拟器和 64 位设备的模拟器所对应的分别是 i386 和 x86_64,它们也是需要分开对待的。
另一种方式是对自定义的符号进行条件编译,比如我们需要使用同一个 target 完成同一个 app 的收费版和免费版两个版本,并且希望在点击某个按钮时收费版本执行功能,而免费版本弹出提示的话,可以使用类似下面的方法:
@IBAction func someButtonPressed(sender: AnyObject!) {
#if FREE_VERSION
// 弹出购买提示,导航至商店等
#else
// 实际功能
#endif
}
在这里我们用 FREE_VERSION 这个编译符号来代表免费版本。为了使之有效,我们需要在项目的编译选项中进行设置,在项目的 Build Settings 中,找到 Swift Compiler - Custom Flags,并在其中的 Other Swift Flags 加上 -D FREE_VERSION 就可以了。

自己实现 :
设置
Switf Compiler - Custom Flags

中 (
active Compilation Conditions (不需要-D) 或者
Other swift flags (需要 -D 前缀)

1
2
3
4
#if DEBUG
// 逻辑
#endif

)debug 选项中加入 -D DEBUG

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
/// 全局函数输入log 条件编译,正式版本不输出
///
/// - Parameters:
/// - message: <#message description#>
/// - file: <#file description#>
/// - method: <#method description#>
/// - line: <#line description#>
func printLog<T>(_ message: T,
file: String = #file,
method: String = #function,
line: Int = #line)
{
// 条件编译还不行 需要在项目中配置条件 Swift Compiler - Custom Flags 加上-D XXX ,debug条件下加
#if DEBUG
print("\((file as NSString).lastPathComponent)[\(line)], \(method): \(message)")
#endif
}

IOS 错误集锦

开发中遇到很多小错误大错误, 很多时候下次遇到可能已经忘记,这里做一个记录,
注:很多是个人的理解不一定是正确答案

1 上传包ERROR ITMS-90206

1
2
3
4
5
ERROR ITMS-90206: "Invalid Bundle. The bundle at 'xxx WatchKit Extension.appex' contains disallowed file 'Frameworks'." I have tried all the ...
这个发现是扩展加载了一些不该加载的,
xcode8 把报错的扩展 build settings 搜索"Always Embed Swift Standard Libraries"
设置成NO
OK 了 上传包不报错了

2 上传包error -22421

原因 苹果服务器抽风多次尝试ok

3 上传包 -4238

原因 自己抽风忘记更改版本号码有重复的


This application’s application-identifier entitlement does not match that of the installed application. These values must match for an upgrade to be allowed

原因:这个项目App已经存在这个真机上了,这个App是旧的identifier运行的。
真机手动删除也ok

解决方法:Xcode —>Window —> Devices —> 自己的真机 —> installed Apps —-> 删除这个App —-> 重新运行


Cannot Synthesize Weak Property Because The Current Deployment Target Does Not Support Weak References

在用 pod 升级依赖(Installing StreamingKit 0.1.30 (was 0.1.29)) 项目后报错:

原因 :
https://github.com/tumtumtum/StreamingKit/blob/master/StreamingKit.podspec
最低支持ios4.3 不支持weak

解决方案:
在 Podfile 下面添加如下代码:

1
2
3
4
5
6
7
8
9
10
post_install do |installer|
installer.pods_project.targets.each do |target|
target.build_configurations.each do |config|
if target.name == 'StreamingKit'
config.build_settings['IPHONEOS_DEPLOYMENT_TARGET'] = '7.0'
end
end
end
end

From: https://zhuanlan.zhihu.com/p/35251092

升级到 Swift 4.1 的两个 Crash

前两天升级了 Xcode 9.3, 也就升级到 Swift 4.1。使用它来编译一些例子工程,源码基本不用修改,只是报了警告,几个地方需要将 flatMap 名字修改成 compactMap。见提案SE-0187

但工程运行起来,出现两个 Crash,记录一下。

1.

Crash 在如下地方,详细情况参见 [Swift/SR-7240]

swiftgetObjectType
UIApplicationDelegate.application(
:open:sourceApplication:annotation:)

原因是

func application(_ application: UIApplication, 
                      open url: URL, 
             sourceApplication: String?, 
                    annotation: Any) -> Bool

已经在 iOS 9.0 中废弃了。需要将其修改为

public func application(_ app: UIApplication, 
                     open url: URL, 
                      options: [UIApplicationOpenURLOptionsKey : Any] = [:]) -> Bool

2.

例子工程使用了一个 HandyJSON 的库,升级到 Swift 4.1 后 Crash 了。

HandyJSON 应该是裁剪使用了一些 Reflection 代码。标准的 Swift Api 还没有完整反射功能的,HandyJSON 的某些接口实际上利用了 Swift 对象,没有公开的内存布局进行赋值,这种做法是一种 Hack 手段,比较危险。

Crash 在

var numberOfFields: Int {
    return Int(pointer.pointee.numberOfFields)
}

修正方法见, Xcode9.3 Swift4.1 crash 解决方案 #239

苹果每次升级,并不追求完全的兼容。升级后,原来的 App 总会有些小问题,强迫着开发者跟进修改。AppStore 很多两三年没有更新的 App, 实际上已经不能运行了。而微软就很讲究兼容性,Win95 上编译好的软件,在 Windows 7 也照样可以跑起来。

打破兼容性,抛弃历史包袱,的确可以使得生态更好。但打破兼容,假若开发者不快速跟进,整个生态早就崩掉了。苹果命好,如此任性。


【iOS 开发】解决使用 CocoaPods 执行 pod install 时出现 - Use the $(inherited) flag … 警告
pod 升级1.5.0 后出了不少警告

1
2
3
4
5
[!] The `boosjdance [Debug]` target overrides the `ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES` build setting defined in `Pods/Target Support Files/Pods-boosjdance/Pods-boosjdance.debug.xcconfig'. This can lead to problems with the CocoaPods installation
- Use the `$(inherited)` flag, or
- Remove the build settings from the target.
[!] The `boosjdance [Release]` target overrides the `ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES` build setting defined in `Pods/Target Support Files/Pods-boosjdance/Pods-boosjdance.release.xcconfig'. This can lead to problems with the CocoaPods installation

解决方法
打开项目 Target - Build Settings ,搜索 Other Linker Flags ,在这个设置上加入 $(inherited) 。

打开项目 Target - Build Settings,依次搜索如下图所示的警告上提示的设置名称,将这些设置选项全部改为 $(inherited) ,或者选中这些设置按下 delete 键恢复原设置。

如果有 FRAMEWORK_SEARCH_PATHS 这个设置的警告的话,最好先把当前的设置项记录下来,然后选中设置按下 delete 以后,再把之前的设置加进去,否则编译可能会出现很多报错。

然后重新执行 pod install 或者 pod update 就会发现警告消失了。

如果我的方法不能够解决你的问题的话,可以试一下网上的另一种方法,就是点击项目文件 project.xcodeproj ,右键显示包内容,用文本编辑器打开 project.pbxproj ,command + F 搜索 OTHER_LDFLAGS ,删除搜索到的设置,command + S 保存,然后重新执行 pod install 或者 pod update 。


更新xcode9.3 出现Block implicitly retains ‘self’;explicitly mention ‘self’ to in dicate this… 警告,

解决 Building Setting -> 搜索
implicit retain of ‘self’
将对应的值改为NO


xcode报错, 系统迁移后, 运行错误,提示权限问题

Permission denied Command PhaseScriptExecution failed with a nonzero exit code

错误提示没有权限,
chmod a+x .sh文件权限 (注意拷贝过来的地址可能需要转译)


UIActionSheet 在ipad中弹不出的问题

UIActionSheet 其实已经弃用,但是我们还在适配ios7 所以还在用着, 遇到过两次ipad 弹不出的问题, 然后时间久了会忘记,特此记录一下
创建用

1
2
3
4
5
6
7
8
9
let actionSheet = UIActionSheet()
actionSheet.delegate = self
actionSheet.actionSheetStyle = UIActionSheetStyle.default
actionSheet.addButton(withTitle: "取消")
actionSheet.addButton(withTitle: "从手机相册选择")
actionSheet.addButton(withTitle: "拍照")
actionSheet.cancelButtonIndex = 0
actionSheet.show(in: (controller?.view)!)

代理方法改用diss 的那个

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
func actionSheet(_ actionSheet: UIActionSheet, didDismissWithButtonIndex buttonIndex: Int) {
if buttonIndex == 0{
return
}
let picker = UIImagePickerController()
if buttonIndex == 1{
picker.sourceType = UIImagePickerControllerSourceType.photoLibrary
}else if buttonIndex == 2{
if UIImagePickerController.isSourceTypeAvailable(UIImagePickerControllerSourceType.camera) == false{
return
}
picker.sourceType = UIImagePickerControllerSourceType.camera
}
picker.delegate = self
picker.allowsEditing = true
self.findController().present(picker, animated: true) {
}
}
// 手机上ok 但是ipad 报错
// func actionSheet(_ actionSheet: UIActionSheet, clickedButtonAt buttonIndex: Int){
// Getdevice.println("clickindex \(buttonIndex)")
// if buttonIndex == 0{
// return
// }
// let picker = UIImagePickerController()
//
// if buttonIndex == 1{
// picker.sourceType = UIImagePickerControllerSourceType.photoLibrary
// }else if buttonIndex == 2{
// if UIImagePickerController.isSourceTypeAvailable(UIImagePickerControllerSourceType.camera) == false{
// return
// }
// picker.sourceType = UIImagePickerControllerSourceType.camera
// }
// picker.delegate = self
// picker.allowsEditing = true
// self.findController().present(picker, animated: true) {
//
// }
// }

Swift中String和Character的使用与总结

使用String字面量给常量赋值

1
2
let string = "string literal value"
//常量string将会自动推断为String类型

初始化一个空的String

1
2
3
var emptyStr = "" //使用空字符串字面量
var anotherEmptyStr = String() //使用构造方法
//两者没有区别

使用isEmpty判断空String:

1
2
3
if emptyStr.isEmpty {
print("have nothing here")
}

String的可变性
使用“+”连接字符串,当然也支持自加运算符”+=”

1
2
3
4
5
6
7
8
var variableStr = "LastName"
variableStr += "and FirstName"
/// variableStr is "LastName and FirstName"
//**but if:
let constantStr = "Gender"
constantStr += "and another Highlander"
///编译器会报错,被声明为常量的字符串不能被修改!

跟oc不同,swift的String通过var/let 变量/常量 标识决定其是否可变(can be mutated),而不需要选择NSString 还是 NSMutableString。

String跟Characters的连接

1
2
3
4
5
let str = "hello world "
let char: Character = "!"
str.append(char)
// 结果str为: "hello world !"

遍历String

1
2
3
4
5
6
7
8
for char in "myStr".characters {
print(char)
}
//m
//y
//S
//t
//r

字符串插值
在字符串中插入常量变量表达式等,构造一个新的字符串”通过()”:

1
2
3
let multiplier = 3
let message = "\(multiplier) times 2.5 is \(Double(multiplier) * 2.5)"
/// message is "3 times 2.5 is 7.5"

String中使用转义字符
在字符串中输入反斜线”\” 水平制表符”t” 换行”n” 双引号”“” 单引号”’” 等都需要在前面添加”\”进行转义,同时可以在转义字符后添加Unicode来进行特殊符号表情的显示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
//**双引号转义
let wiseWords = "\"Imagination is more important than knowledge\" - Einstein"
///"Imagination is more important than knowledge" - Einstein
//**Unicode转义
let dollarSign = "\u{24}" // $, Unicode scalar U+0024
let blackHeart = "\u{2665}" // ♥, Unicode scalar U+2665
let sparklingHeart = "\u{1F496}" // ��, Unicode scalar U+1F496
//**扩展自行集
//**对应关系
// \u{D55C}----한
// \u{1112}----ᄒ
// \u{1161}----ᅡ
// \u{11AB}----ᆫ
let KoreaStr = "\u{D55C}\u{1112}\u{1161}\u{11AB}" //한한

String长度
string.characters.count

1
2
3
4
5
6
7
8
9
10
11
12
13
let str = "1234567890"
print("str has \(str.characters.count) characters")
//输出 "star has 10 characters"
//**为String增加笔画不会造成长度增加:
var str = "cafe"
print("the number of characters in \(word) is \(word.characters.count)")
// 输出 "the number of characters in cafe is 4"
//**now append some Unicode:
word += "\u{301}"
print("the number of characters in \(word) is \(word.characters.count)")
//输出 "the number of characters in café is 4"
//仅仅是改变了最后一个字符,并没有增加字符串的长度

正因为swift支持扩展字形集,不同的字符,和相同的不同表示的字符可能需要不同量的存储器来存储,所以在swift中characters所占用的存储量是不一定相同的,因此不能像oc计算NSString那样使用字符串来迭代计算,而应该遍历字符串的characters来确定字符串的长度。

####访问和修改字符串
可以通过其方法和属性,或者下标,来访问或者修改字符串

###字符串索引
swift中的字符串具有相关连的索引类型(String.Index),可对应其每个位置的Character

正如上面所说,不同的字符串可能需要不同数量的内存来存储,所以为了确定哪些character在特定的位置上,我们必须遍历确定每个Unicode的开始结束位置,因此,String不能使用整形作索引。

startIndex: 访问String第一个位置的字符 endIndex: 访问String最后一个位置的字符
(一个空的字符串或者长度为1的字符串,startIndex和endIndex相等)

predecessor(), successor(), advancedBy() 一个String.Index值可以通过调用predecessor()方法来访问其前一个index, 调用successor()来访问其后一个index, 或者调用advancedBy()来指定访问相对位置的index( 之后5位的index: advancedBy(5) 往前5位的index: advancedBy(-5) )

1
2
3
4
5
6
7
8
9
10
11
let greeting = "Guten Tag!"
greeting[greeting.startIndex]
//G
greeting[greeting.endIndex.predecessor()]
//!
greeting[greeting.startIndex.successor()]
//u
let index = greeting.startIndex.advancedBy(7)
//a
greeting[index]
//输出 a

indiced : 字符串Index的集合

1
2
3
4
for index in greeting.characters.indices {
print("\(greeting[index])", terminator: " ")
}
///prints "G u t e n T a g !"

插入/移除
利用index,在制定位置插入字符character

1
2
3
var helloStr = "hello"
helloStr.insert("~", atIndex: helloStr.endIndex)
// hello~

同理,插入字符串(字符的集合)

1
2
3
4
5
6
7
8
var helloStr = "hello!"
helloStr.insertContentOf(" world!".characters, at: hello.endIndex)
// hello! world
//用上面的知识,再追求下完美:
var helloStr = "hello!"
helloStr.insertContentOf(" world".characters, at: hello.endIndex.predecessor())
// hello world!

移除(index):

1
2
3
4
5
6
7
var helloStr = "hello world!"
helloStr.removeAtIndex(helloStr.endIndex.predecessor())
// hello world
//注意:
// endIndex是指最后一个index位(将要输入内容的index位)
//所以删除最后一个字符使用的index是endIndex.predecessor()(将要输入内容的index的前一个index位)
//而不是endIndex

移除(Range):

1
2
3
4
5
6
7
var helloStr = "hello world!"
let range = Range(start: helloStr.endIndex.advancedBy(-6), end: helloStr.endIndex.predecessor())
// 顺便贴一个new Range的简易写法:
// let range = helloStr.endIndex.advancedBy(-6)..<helloStr.endIndex
// 效果是一样的
helloStr.removeRange(range)
// hello

####字符串比较
两个纯字符串比较

1
2
3
4
5
6
7
8
let oneStr = "We're a lot alike, you and I."
let anotherStr = "We're a lot alike, you and I."
if oneStr == anotherStr {
print("These two strings are considered equal")
}
//输出: These two strings are considered equal
//相等

两个由characters组成的字符串比较

1
2
3
4
5
6
7
8
9
10
11
let oneStr = "Voulez-vous un caf\u{E9}?"
//Voulez-vous un café?
let anotherStr = "Voulez-vous un caf\u{65}\u{301}?"
//Voulez-vous un café?
//两者虽然看起来内容字符不同,其实\u{65}\u{301}是一个e和一个音调符号,根据上面的知识,结果组合成é(\u{E9})
if oneStr == anotherStr {
print("These two strings are considered equal")
}
//输出: These two strings are considered equal
//相等

两个表现相同的character比较

1
2
3
4
5
6
7
8
9
10
let oneChar: Character = "\u{41}"
//拉丁字母中的A
let anotherChar: Character = "\u{0410}"
//西里尔字母中的A
if oneChar != anotherChar {
print(These two characters are not equivalent)
}
//输出: These two characters are not equivalent
//不相等!

前缀和后缀的比较 我们可以使用hasPrefix()方法和hasSuffix()去匹配String的前缀和后缀,并返回一个Boolean值

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
let romeoAndJuliet = [
"Act 1 Scene 1: Verona, A public place",
"Act 1 Scene 2: Capulet's mansion",
"Act 1 Scene 3: A room in Capulet's mansion",
"Act 1 Scene 4: A street outside Capulet's mansion",
"Act 1 Scene 5: The Great Hall in Capulet's mansion",
"Act 2 Scene 1: Outside Capulet's mansion",
"Act 2 Scene 2: Capulet's orchard",
"Act 2 Scene 3: Outside Friar Lawrence's cell",
"Act 2 Scene 4: A street in Verona",
"Act 2 Scene 5: Capulet's mansion",
"Act 2 Scene 6: Friar Lawrence's cell"
]
//----遍历这个字符数组,匹配下前缀看看效果
var count = 0
for str in romeoAndJuliet {
if str.hasPrefix("Act 1 ") {
count++
}
}
print("There are \(count) string with Act 1 ")
// 输出: "There are 5 string with Act 1"
//----后缀呢
var count = 0
for str in romeoAndJuliet {
if str.hasSuffix("Capulet's mansion") {
count++
}
}
print("There are \(count) mansion string")
// 输出: "There are 6 mansion stressing"

String使用UTF-8编码表示
复习一下,上面也提到,Swift中的String支持emoji表情和众多特殊字符,这也是String一个单位长度不一定等于两个character(汉字)或者1个character(英文字母)的原因。 先回到我们的话题。String和UTF-8的对应关系,我们来看一张官方电子书中的表: ![]/content/images/2015/12/utf8.png()

1
2
3
4
5
6
7
8
//上图中对应的String:
//let dogString = "Dog!!��"
//同时String中的UTF-8编码也是可以像char那样遍历的
for unitCode in dogString.utf8 {
print("\(unitCode) ", terminator: "")
}
//输出: 68 111 103 226 128 188 240 159 144 182

同理String也可以以UTF-16 和Unicode的方式遍历

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
for unitCode in dogString.utf16 {
}
for scalar in dogString.unicodeScalars {
print("\(scalar.value) ", terminator: "")
}
// 68 111 103 8252 128054
for scalar in dogString.unicodeScalars {
print("\(scalar) ", terminator: "")
}
// D o g !! ��
//注意: 直接printunicodeScalar的话跟String的输出是一样效果的
//我们print出他的value,才是我们想要的编码

copy 自:
http://zyden.vicp.cc/string-character/