关于字符串计算size的方法比较

往往字符串需要计算size来满足UI排版的自适应的需要,而一般字符串也是放在UILabel里的。 而在计算size的方法里,一般有两种方式(deprecated的就不说了)。

NSString的方法 boundingRectWithSize:optoins:attributes:context: 和 UILabel的textRectForBounds:limitedToNumberOfLines 两个方法,那么计算结果有差距吗?我测试了下 ,如下图,结果误差不大,基本可以直接用来赋值label的长宽。

测试代码

http://7xk2xq.com1.z0.glb.clouddn.com/blogphotos/lALOA3F0ZM0BLM0DtQ_949_300.png

测试结果

http://7xk2xq.com1.z0.glb.clouddn.com/blogphotos/lALOA3F0Y8yGzQIL_523_134.png

Runtime Programming Guide再次研究

这次准备好好研究下runtime programming, 先从encodings开始了解,这个对于理解oc的类也是很有帮助的。 废话不多说直接开始。

Type Encodings

compiler 编码encode 每个method的返回值,参数类型,将这些信息保存在一个字符串里。然后将这个string和selector关联起来。

这套编码也适用于其他场景,所以这个@encode编译器指令是设计成公开的。当给定一个type的规范声明,@encode()返回相对应的字符串类型编码值。

1
2
3
char *buf1 = @encode(int **);
char *buf2 = @encode(struct key);
char *buf3 = @encode(Rectangle);

type encodings 更多的类型可以查看https://developer.apple.com/library/ios/documentation/Cocoa/Conceptual/ObjCRuntimeGuide/Articles/ocrtTypeEncodings.html#//apple_ref/doc/uid/TP40008048-CH100-SW1

数组的type code是一个中括号里面,然后紧接着一个数组个数值和相对应的数组元素的类型,如一个包含12个float指针的数组的编码为:[12^f]

structures 是一个大括号里,里面是显示紧跟着structure tag(struct的结构名称)然后一个等号(equal sign)=,接着就是struct里各个类型排列在一起,比如

1
2
3
4
5
typedef struct example {
    id   anObject;
    char *aString;
    int  anInt;
} Example;

的type code值为 {example=@*i}, 如果是一个结构体的指针,那么encode为^{example=@*i},如果是一个结构体的指针的指针,则是^^{example}.

Objects对象也被看成一个struct。@encode(NSObject)结果是{NSObject=#}, 因为NSObject class里就一个isa(class 类型)的变量。

尽管 @encode()不直接返回数据, 但运行时会使用额外的一个encoding列表来表示一些方法声明的后缀修饰符.

code Meaning
r const
i int
N inout
o out
O bycopy
R byref
V oneway

Declared Propertyies

当编译器碰到property声明时,它会生成描述性的带有关于class, category或者protocol的metadata元数据。

你能通过一些列的api函数来访问这个metadata,通过class,protocol上的name来查找property。可以通过@encode来获取property的encode string。可以copy property的attributes list(C strings)。

class和protocol都有一个properties list的值。

property type and functions

可以通过class_copyPropertyList和protocol_copyPropertyList来遍历class(包括loaded categories)和protocol的property list

通过property_getAttributes可以获取到一个property属性的@encode type的值。

比如一个@property (nonatomic, strong) NSString *login;的property @encode值为T@"NSString",&,N,V_login

每个逗号都是分割一个属性attribute,实际上当你获取到objc_property_attribute_t *attrs = property_copyAttributeList(prot, &outcount);(注意你还需要主动free掉attrs) outcount的值就是分割后的数组的个数。

attrs分别为:

property index name value
0 T @“NSString”
1 &
2 N
3 V _login

Property type string

文档里面有一段英文很重要,他描述了property type的string值的格式(这个值可以通过@encode或者通过property_getAttributes来获取)。

You can use the property_getAttributes function to discover the name, the @encode type string of a property, and other attributes of the property.

The string starts with a T followed by the @encode type and a comma, and finishes with a V followed by the name of the backing instance variable. Between these, the attributes are specified by the following descriptors, separated by commas:

这段英文很重要,说明了格式,首先是以一个T开头的,然后@encode type加一个逗号,以一个V后面带上存储支持的实例变量,然后在中间,每个attributes都是以逗号隔开的,这些attributes描述如下:

可以官场上面的那个例子@property (nonatomic, strong) NSString *login;的property @encode值为T@"NSString",&,N,V_login

Table 7-1 Declared property type encodings

Code Meaning
R The property is read-only (readonly).
C The property is a copy of the value last assigned (copy).
& The property is a reference to the value last assigned (retain).
N The property is non-atomic (nonatomic).
G The property defines a custom getter selector name. The name follows the G (for example, GcustomGetter,).
S The property defines a custom setter selector name. The name follows the S (for example, ScustomSetter:,)
D The property is dynamic (@dynamic).
W The property is a weak reference (__weak).
P The property is eligible for garbage collection.
t Specifies the type using old-style encoding.

这个链接里有相关的demo示例说明property declaration声明和描述description。 https://developer.apple.com/library/ios/documentation/Cocoa/Conceptual/ObjCRuntimeGuide/Articles/ocrtPropertyIntrospection.html#//apple_ref/doc/uid/TP40008048-CH101-SW5

这里只罗列少数几个:

property declaration description
@property(getter=intGetFoo, setter=intSetFoo:) int intSetterGetter; Ti,GintGetFoo,SintSetFoo:,V_intSetterGetter
@property int intSynthEquals;In the implementation block:@synthesize intSynthEquals=_intSynthEquals; Ti,V_intSynthEquals
@property int (*functionPointerDefault)(char *); T^?,V_functionPointerDefault
@property (strong, atomic) NSString * dataObject; T@“NSString”,&,V_dataObject

function/method type encode

先看例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
//- (void)viewWillAppear:(BOOL)animated  -> v20@0:8B16
NSLog(@"%s", class_getInstanceMethod([self class], @selector(viewWillAppear:)));
//- (void) setSomething:(id) anObject  -> v24@0:8@16
NSLog(@"%s", method_getTypeEncoding(class_getInstanceMethod([self class], @selector(setSomething:))));
//- (BOOL) setSomething:(UInt8)animated aa:(id)anObject  -> B28@0:8C16@20
NSLog(@"%s", method_getTypeEncoding(class_getInstanceMethod([self class], @selector(setSomething:aa:))));
//- (BOOL) setSomething:(UInt16)animated aa:(id)anObject  -> B28@0:8s16@20
NSLog(@"%s", method_getTypeEncoding(class_getInstanceMethod([self class], @selector(setSomething:aa:))));
//- (BOOL) setSomething:(int)animated aa:(id)anObject  -> B28@0:8i16@20
NSLog(@"%s", method_getTypeEncoding(class_getInstanceMethod([self class], @selector(setSomething:aa:))));
//- (BOOL) setSomething:(long)animated aa:(id)anObject-> B32@0:8q16@24
NSLog(@"%s", method_getTypeEncoding(class_getInstanceMethod([self class], @selector(setSomething:aa:))));


typedef struct  {
    uint32_t a;
    uint16_t b;
    uint8_t c;
} __attribute__((packed)) MyStruct;
// - (void)strutMethod:(MyStruct)aa  -> v23@0:8{?=ISC}16
NSLog(@"%s", method_getTypeEncoding(class_getInstanceMethod([self class], @selector(strutMethod:))));

那么如何理解呢?

  • v means void return type
  • 20 means the size of the argument frame (20 bytes),整个方法参数占位的总长度
  • @0 means that there is an Objective-C object type at byte offset 0 of the argument frame (this is the implicit self object in each Objective-C method),这个表示在offset为0的地方有一个objective-c的对象,在objective-c method里,首个对象是self自身。
  • :8 means that there is a selector at byte offset 8 (this is the implicit _cmd in every method, which is the selector that was used to invoke the method). 在offset为8的地方有一个SEL,由于我测试的是64位机器上,所以之前的OC的对象指针占位8个字节。
  • B16 means 在offset 16的地方,有一个bool类型的参数,由于oc的对象和sel都是指针类型,64位机下所以都是占位8位,这里的bool也就出现在offset16的地方了。至于为什么bool参数占位了4个字节。主要原因是内存对齐的原因

从上面看起来,不管是bool,uint8,uint16,int都被认为4个字节。因为他们的内存实际占位都小于4个字节,由于内存对齐原因所以实际最后占位为4字节。这里当你看最后一个MyStruct就可以看出,当主动告诉编译器取消结构体的内存对齐,就发现最后一个参数事迹展位是7(4+2+1)个字节

1
2
3
4
5
6
注:packed属性:使用该属性可以使得变量或者结构体成员使用最小的对齐方式,即对变量是一字节对齐,对域(field)是位对齐.
typedef struct  {
    uint32_t a;
    uint16_t b;
    uint8_t c;
} __attribute__((packed)) MyStruct;

未完待续

至此先了解介绍完了关于type encodings, property type encodings,method encodings。稍后继续补充了解更多的运行时编程相关的知识点,runtime programming guide~~~.

参考链接

  1. http://lists.cs.uiuc.edu/pipermail/cfe-dev/2014-April/036654.html

CloudKit访问权限

CloudKit在使用的时候就和通讯录,照片等访问一样,都需要用户类似授权的行为,当然CloudKit略有不通,它需要用户的设备的OS版本至少8.0一样,然后用户还需要设置自己的iCloud账号,而且还必须在设置里不关闭app对iCloud Drive的访问(开关路径为:设置->iCloud->iCloud Drive),否则用户可能出现完全无法访问iCloud或者可能读数据,而无法保存或者更新数据。

那么iCloud的登陆状态,开关等的状态的设置会在app里有什么样的返回结果呢?

测试结果

场景 CKAccountStatus状态 CKApplicationPermissionStatus值
关闭iCloud,完全不登陆iCloud账号 NoAccount CouldNotComplete
登陆iCloud,关闭iCloud Drive NoAccount CouldNotComplete
登陆iCloud,打开iCloud Drive,关闭app的访问权限 NoAccount CouldNotComplete
全打开 Available CouldNotComplete

实际上CKApplicationPermissionStatus的授权需要调用requestApplicationPermission:completion:,当用户完成授权后,这里的CKApplicationPermissionStatus会变为CKApplicationPermissionStatusGranted。而这个授权是为了特殊的一些权限,主要是通过email能看到应用。具体没深入了解

理解SSL,TLS和HTTPS

TLS(Transport Layer Security Protocol):安全传输层协议

SSL运行在TCP/IP层之上、应用层之下

SSL

SSL 安全套接字层. SSL介于应用层和TCP层之间。应用层数据不再直接传递给传输层,而是传递给SSL层,SSL层对从应用层收到的数据进行加密,并增加自己的SSL头。勉强可以划到传输层,

在SSL中会

1. 使用密钥交换算法交换密钥;
2. 使用密钥对数据进行加密;
3. 使用散列算法对数据的完整性进行验证
4. 使用数字证书证明自己的身份。

特性:

1. 保密安全:在握手协议中定义会话密钥后,所有的消息都是被加密传输的。
2. 鉴别身份,可选的客户端认证,和强制的服务端认证。
3. 防篡改的完整性,传输数据的数据完整性检查。

好了,下面开始介绍SSL协议。

SSL工作原理

分为记录协议,握手协议,警报协议。

握手协议是在应用程序数据传输之前使用的,主要的目的是为了互相验证,协商加密算法和加密密钥等信息(用来保护SSL记录里的发送的数据的安全性)

握手协议包含一下三个字段

  1. Type: 表示10种消息类型之一
  2. length:表示消息体长度字节数
  3. content:与消息相关的参数

SSL握手协议字段

握手过程(4个阶段)

  1. 客户端向服务端发送Client Hello,携带信息有:协议版本号,客户端随机数Client Random A,客户端支持的加密方法列表
  2. 服务端根据客户端支持的加密算法选择确认双方使用的加密方法,然后将这个加密方法信息,再带上数字证书(certificate),服务端随机数Server Random B,返回给客户端,最终发送Server Hello Done。(注意这里其实还有可选的进一步校验,比如如果服务端要求验证客户端更多的信息,可以发送certificate request,还有server key exchange。)
  3. 客户端确认服务端返回的证书有效性,然后再根据AB两个随机数再生成一个新的随机数(Premaster Secret),然后使用证书中的公钥对这个Premaster Secret加密,发送给服务端。(注意,这里也可选的可以有一个server key exchange)
  4. 服务端通过私钥解析出客户端生成的Premaster Secret,然后根据约定的加密方法,使用A,B,Premaster Secret三个随机数,来生成最终的”对话密钥”(Session Key), 用来加密接下来的整个会话过程里的数据了。

这里A,B都是不安全的,生成session key的方法是公开的,关键的key在于premaster secret,这个被认为最具有随机性的一个key,通过A+B+PremasterKey生成session key的公开算法,就得到了最后的秘钥了。 用图来说明的话,如下:

SSL Handshake

Session的恢复

握手阶段用来建立SSL连接。如果出于某种原因,对话中断,就需要重新握手。 这时有两种方法可以恢复原来的session:一种叫做session ID,另一种叫做session ticket。

TLS

TLS 握手协议提供的连接安全具有三个基本属性:
可以使用非对称的,或公共密钥的密码术来认证对等方的身份。该认证是可选的,但至少需要一个结点方。 共享加密密钥的协商是安全的。对偷窃者来说协商加密是难以获得的。此外经过认证过的连接不能获得加密,即使是进入连接中间的攻击者也不能。 协商是可靠的。没有经过通信方成员的检测,任何攻击者都不能修改通信协商。

TLS的最大优势就在于:TLS是独立于应用协议。高层协议可以透明地分布在TLS协议上面。然而, TLS 标准并没有规定应用程序如何在TLS上增加安全性;它把如何启动 TLS 握手协议以及如何解释交换的认证证书的决定权留给协议的设计者和实施者来判断。

TLS记录协议是一种分层协议。每一层中的信息可能包含长度、描述和内容等字段。记录协议支持信息传输、将数据分段到可处理块、压缩数据、应用MAC 、加密以及传输结果等。对接收到的数据进行解密、校验、解压缩、重组等,然后将它们传送到高层客户机。 参考链接:

在TLS与SSL3.0之间存在着显著的差别,主要是它们所支持的加密算法不同,所以TLS与SSL3.0不能互操作。

TLS与SSL的差异

  1. 版本号:TLS记录格式与SSL记录格式相同,但版本号的值不同,TLS的版本1.0使用的版本号为SSLv3.1。

  2. 报文鉴别码:SSLv3.0和TLS的MAC算法及MAC计算的范围不同。TLS使用了RFC-2104定义的HMAC算法。SSLv3.0使用了相似的算法,两者差别在于SSLv3.0中,填充字节与密钥之间采用的是连接运算,而HMAC算法采用的是异或运算。但是两者的安全程度是相同的。

  3. 伪随机函数:TLS使用了称为PRF的伪随机函数来将密钥扩展成数据块,是更安全的方式。

  4. 报警代码:TLS支持几乎所有的SSLv3.0报警代码,而且TLS还补充定义了很多报警代码,如解密失败(decryption_failed)、记录溢出(record_overflow)、未知CA(unknown_ca)、拒绝访问(access_denied)等。

  5. 密文族和客户证书:SSLv3.0和TLS存在少量差别,即TLS不支持Fortezza密钥交换、加密算法和客户证书。
  6. certificate_verify和finished消息:SSLv3.0和TLS在用certificate_verify和finished消息计算MD5和SHA-1散列码时,计算的输入有少许差别,但安全性相当。
  7. 加密计算:TLS与SSLv3.0在计算主密值(master secret)时采用的方式不同。
  8. 填充:用户数据加密之前需要增加的填充字节。在SSL中,填充后的数据长度要达到密文块长度的最小整数倍。而在TLS中,填充后的数据长度可以是密文块长度的任意整数倍(但填充的最大长度为255字节),这种方式可以防止基于对报文长度进行分析的攻击。

TLS 的最大优势

TLS 的最大优势就在于:TLS 是独立于应用协议。高层协议可以透明地分布在 TLS 协议上面。然而,TLS 标准并没有规定应用程序如何在 TLS 上增加安全性;它把如何启动 TLS 握手协议以及如何解释交换的认证证书的决定权留给协议的设计者和实施者来判断。 TLS包含三个基本阶段: 1.对等协商支援的密钥算法 2.基于私钥加密交换公钥、基于PKI证书的身份认证 3.基于公钥加密的数据传输保密

HTTPS是什么

通常所说的 HTTPS 协议,说白了就是“HTTP 协议”和“SSL/TLS 协议”的组合。你可以把 HTTPS 大致理解为——“HTTP over SSL”或“HTTP over TLS”(反正 SSL 和 TLS 差不多)。

加解密

对称加密

  • 对称加密指的是可以使用同一个密钥对内容进行加密和解密。相比非对称加密,它的特点是加、解密速度快,并且加密的内容长度几乎没有限制,简单的表达式就是

发送者加密信息:encrypted_message = func1(message, key),

接收者解密信息:message = func2(encrypted_message, key)

其中根据不同的算法,func1有可能会等于func2,也有可能不同。 IC168364

  • 现在常用的对称密钥主要分成两种,块式加密和流式加密,他们的基本思想都是对信息进行XOR、移位等操作来进行加密的。在操作方式上块式加密是把message分成多个固定长度的组,每组包含多个字节,每次操作的时候会针对一组字节进行操作,鼎鼎大名的DES和AES就是采用这类方式。而流式加密是每次只针对一个字节进行操作,鼎鼎大名的RC4就是采用这种方式。

  • DES的原理

    • 我们将需要加密的消息分成长度为64bits的一堆block,对每个block通过XOR、移位等等方式进行加密,DES的密钥(key)长度是64bits,有效长度是56bits,因为其中的8个bits用于校验。另外DES有一个特点就是对于同一个block,如果使用同一个密钥(key),加密出来的结果是相同的,换句话说攻击者可以通过寻找同样的密文block来推理出原文,这就是我们常说的回放攻击。
    • 为了解决回放攻击的问题,最简单的方式就是CBC(cipher block chaining),简单说来就是每次加密一个block之后,把它和前一个block的密文进行XOR操作,作为这个block的密文,这样即便是同样的block,每次加密的结果也会不一样,攻击者就无法通过密文猜测原文了。另外第一个block因为在它之前没有其他block了,所以我们需要生成一个随机的64bit的intialization vector(类似我们在写程序对用户密码加密的时候搞的salt)给第一个block进行XOR操作。 601px_CBC_encryption_svg
    • 3DES是在DES的基础上将密钥扩充为原来的3倍也就是192bits(实际有效密钥长度是56bits*3=168bits),3DES的加密过程其实就是将密钥分成三份,使用第一份密钥加密、第二份密钥解密、第三份密钥加密,然后解密的话刚好相反,使用第三份密钥解密,第二份密钥加密和第一份密钥解密,通过这种方式去扩展DES的加密长度问题,3DES更加安全,也能抵挡之前说的回放攻击。
  • AES的原理

    • 3DES虽然很安全,但是因为多次的加解密导致性能其实很低的,所以数学家们希望在同样的密钥长度下使用更高效率的算法,于是有了AES。
    • AES允许密钥长度为128-bit,192-bit和256-bit,当前来说使用128-bit已经足够安全,并且性能比3DES快很多,所以现在使用最多的对称加密就是AES。
  • RC4的原理

    • 如果把block的size变成了1 byte那么就变成了stream加密算法,当然具体的实现肯定不一样,block式加密主要是将block里面的每个bit进行移位,而stream加密是生成同样长度的、安全的字符串,然后再和原文进行XOR生成最终的密文,所以stream的安全性主要取决于这个安全字符串的生成算法,同样因为RC4安全性取决于安全字符串的生成,所以它不需要CBC或者IV。 总的来说对称加密性能高、速度快,但是它的前提是必须双方都知道这个共同的密钥,而这个密钥又不可以被第三方知道,否则他们也是可以解密密文。为了解决密钥传输的问题,数学家又发明了非对称加密的算法,相比而言,非对称加密解决了密钥本身的安全传输问题,但是他的性能相比对称加密来说相差几万倍,而且对所加密的内容也有长度的限制。

参考链接

  1. http://www.ruanyifeng.com/blog/2014/09/illustration-ssl.html

iOS里的栈限制引发的crash

昨天一个开发说,app一启动就崩溃,而且是添加了一个比较变态的测试数据后引发的,调试后发现崩溃在IDL的解析代码里,结果分析测试发现原来问题出现在iOS栈上。

问题分析

代码如下:

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
-(NSData *) readRaw: (uint8_t) t
{
    int32_t len = 0;
    if ((t & 0xe0) == 0xa0) { // FixArray
        len = t & 0x1f;
    } else if ((t & 0xff) == 0xda) { // raw 16
        len = (uint16_t)[inputStream readShort];
    } else if ((t & 0xff) == 0xdb) {// raw 32
        len = (uint32_t)[inputStream readInt];
    }
    if (len == 0) {
        return nil;
    }
    uint8_t buffer[len];
    [inputStream readBytes:buffer length:len];
    return [NSData dataWithBytes:(const char *)&buffer length: len];
}


// read
-(unsigned int) readBytes: (uint8_t *)buff length: (unsigned int) len
{
    if(len <= 0){
        @throw [NSException exceptionWithName:@"PackException" reason:@"args is illegal" userInfo:nil ];
    }
    if (totalSize - mOffset < len) {
        @throw [NSException exceptionWithName:@"EOFException" reason:@"Not enough bytes remain in buffer" userInfo:nil ];
  }
  [mBuffer getBytes:buff range:NSMakeRange(mOffset, len)];//<===崩溃在这里。
  mOffset += len;
    if (mOffset >= GARBAGE_BUFFER_SIZE) {
      [mBuffer replaceBytesInRange:NSMakeRange(0, mOffset) withBytes:NULL length:0];
      mOffset = 0;
        totalSize = [mBuffer length];
  }
  return len;
}

error错误是bad access,而从-(unsigned int) readBytes: (uint8_t *)buff length: (unsigned int) len函数本来来看基本很难看出来,所以需要往上看一个函数调用,从代码本身来看也还算正常,当然细心的人可能会看到这里有个比较危险的代码,没错就是它。

1
2
3
4
5
if (len == 0) {
    return nil;
}
uint8_t buffer[len];      //<====就是它
[inputStream readBytes:buffer length:len];

len的大小其实是不可控的,是由服务端返回的一个值,用来表示msgpack后面的某个字段的数据的长度。而这里使用了栈申请内存,此时就面临万一len大于一定值后,导致申请内存会破坏栈了。

当然在我们申请内存的时候并不会崩溃,毕竟这里还没有开始写操作,而当你调用[mBuffer getBytes:buff range:NSMakeRange(mOffset, len)];方法的时候,就棉铃将数据mBuffer里的数据copy到buff对象里了,此时就会将栈上的数据给破坏掉了,从而导致崩溃。因此也就解释了为什么崩溃是在后面的方法里。

处理结果

既然知道了是栈申请大内存导致栈被破坏掉了而引发的崩溃,所以自然需要将栈内存改成堆内存就可以了。在不大规模修改代码的情况下,便有了下面的fix代码

 uint8_t *buffer = malloc(len+1);
memset(buffer, 0, sizeof(buffer));
[inputStream readBytes:buffer length:len];
return [NSData dataWithBytesNoCopy:(void *)buffer length:len];

为了尽量少的内存拷贝,首选dataWithBytesNoCopy。

结论

那么到底这个栈有多大呢?平时碰到的,google了下,结论是:

iOS上主线程栈大小1MB,其他线程512K,OSX上主线程栈大小8M

stack size limit

更多详细请看:

https://developer.apple.com/library/ios/documentation/Cocoa/Conceptual/Multithreading/CreatingThreads/CreatingThreads.html

另外说明下,对于不可知大小的内存申请,本来也不应该通过栈申请内存来做数据处理。

通过NSURLProtocol拦截HTTP转HTTPS来整合SPDY的记录

众所周知,iOS 9.0之后苹果引入ATS限制,苹果也推荐尽量不要使用HTTP通讯了,毕竟是很不安全的。而国内各个有(wu)节操的运营商也会经常篡改请求HTTP请求。所以如果可能,在不影响性能的情况下,使用https总是更好一点。但是移动网络下HTTPS的握手耗时,也总是很让人难已接受。那么考虑整合Spdy来减少握手时间的损耗,复用链接来进行通讯,是一个不错的尝试。

但对于老的app,尤其是本地已经存储了大量的老旧URL来说,尝试数据升级将本地数据库里的各种http转成https的操作也是令人发指的,尤其是这种操作很不适合做灰度测试发布。总不能数据改来改去,对于那些可能本地存了几万几十万条消息记录的app来说,简直是灾难。因此如何寻求在老的app上完成http的请求转https的请求,以及整合spdy来减少握手时间,提升弱网效果就显的很重要,而我们应该寻找优雅的解决方式来完成过渡。

注:这里不讨论为什么不用HTTP2.0,不选择总有不选择的原因,再换个角度,你完成了spdy的接入,HTTP2.0的接入从大体思路上是类似的。

注:由于NSURLProtocol的拦截及再发送,涉及的坑很多,这里先不讲了,大家也可以参考下这个,里面已经埋了不少坑。

Spdy的选择及注意事项

对于iOS来说,现有的spdy开源库,暂时可以考虑CocoaSpdy, 说起Spdy,大家第一反应一般是多路复用请求(multiplexing requests),头部压缩等特性,其实Spdy的设计里充分考虑了cancel的需要,这个特性其实也是非常重要的,否则复用链接会引入一个灾难的问题(就是上层已经cancel的请求在复用链路中堆积而影响后续各种请求)。

注:以下我们考虑的是采用NSURLConnection的请求的拦截。

CocoaSpdy的设计里采用在SPDYURLConnectionProtocol的load函数里将自己注册到NSURLProtocol里,作为独立的第三方库,这个可以帮你省却一些烦恼,快速接入Spdy。

但是当你发现你的app可能因为已经引入另一个NSURLProtocol的子类来做流量统计,缓存命中,甚至HTTPDNS的转换(不了解这块的可以查看下这里)的时候,那么默认的load自动注册可能会引发拦截顺序问题,所以这里我还是注释掉了cocoaspdy的这个代码。改由自己手动注册,注意如果你要关注下注册的顺序和生效的顺序,先注册的后生效。

接入基本步骤:

1
2
3
4
1. 注册SPDYURLConnectionProtocol
2. 注册original,即针对哪个scheme,host,port进行拦截
3. 设置logger的delegate和logLevel,另外可以考虑设置并发数。
4. 等待相关的URL请求触发相关protocol的拦截,CocoaSpdy内部会解析请求,并进行相关请求及返回。

也可以看下如下的基本代码

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
// 先注册spdy的protocol
[NSURLProtocol registerClass:[SPDYURLConnectionProtocol class]];
// 下面这个可以先忽略,用于拦截http转https等用处
[NSURLProtocol registerClass:NSClassFromString(@"ILURLProtocol")];

// 注册拦截的规则,这里是预注册,如果你使用过程中有新引入一些域名,也可以临时再添加注册
[SPDYURLConnectionProtocol registerOrigin:@"https://static.dingtalk.com"];
// log的delegate和loglevel
[SPDYProtocol setLogger:self];
[SPDYProtocol setLoggerLevel:SPDYLogLevelDebug];
// 设置pool的并发数,注意,这个是针对每个origin的的size。
SPDYConfiguration *configration = [SPDYProtocol currentConfiguration];
configration.sessionPoolSize = 3;
[SPDYProtocol setConfiguration:configration];


完成后,就可以通过下载https图片来观察下载情况了(注意spdy的日志打印)
比如引入sdwebimage显示图片,

NSString imageurl = @"https://static.dingtalk.com/media/lADOAaoJdM0FNs0D6A_1000_1334.jpg_790x10000g.jpg";
[cell.imageView sd_setImageWithURL:[NSURL URLWithString:imageurl]
                  placeholderImage:[UIImage imageNamed:@"lock"]
                           options:SDWebImageRefreshCached|SDWebImageCacheMemoryOnly
                         completed:^(UIImage *image, NSError *error, SDImageCacheType cacheType, NSURL *imageURL) {
                                 NSLog(@"%@ %@ %ld", image, error, (long)cacheType);
                             }];

Spdy的一些基本概念和关系

Spdy在SSL层上加了一个SPDY session 层,来实现并发和stream机制。对CocoaSpdy来说,也就有SPDYSessionManager的概念,每个origin(SPDYStream)都有一个session manager(SPDYSessionManager),管理session pool和stream队列,每个session(SPDYSession,相当于一条spdy链接)都可以用来发送stream,每个stream就意味着上层发起的一个request,cancel一个请求,实际上是cancel一个stream,cancel也不是简单的移除stream,更会向服务端发起cancel stream的操作。服务端收到后会停止继续推送当前正在处理的stream请求的数据。这对于大数据量的文件下载尤为重要。

HTTP进行HTTPDNS和HTTPS的URL拦截转换

我们需要拦截HTTP,也要拦截那些指定域名被HTTPDNS之后的请求,比如 @“http://static.dingtalk.com/media/lADOAAJMIs0CgM0CgA_640_640.jpg_90x90.jpg” 经过httpdns处理之后,获取到的ip可能为 @“http://110.75.113.81/media/lADOAAj5i80CgM0CgA_640_640.jpg_90x90.jpg“,host填为static.dingtalk.com。 此时我们同样可以将这个http://110.75.113.81/media/lADOAAj5i80CgM0CgA_640_640.jpg_90x90.jpg 拦截下来(通过在request header里的host判断,当然对于spdy来说,你还需要注册对于110.75.113.81的拦截,而且最好还要添加对于host里的域名判断)。

这里还涉及到如何让spdy支持对IP类型的地址进行https握手建连的问题,这里需要说明的是,服务端配spdy的时候需要支持SNI的扩展,客户端在ssl建连的时候也需要主动握手参数添加kCFStreamSSLPeerName值为host来覆盖用于证书校验的名字。

我修改了下CocoaSpdy里的_tryTLSHandhshake的部分代码,从而让ip类型的服务器地址支持https握手建连。

1
2
3
4
5
6
// 注:SpdyOrigin类被改造支持domainHost参数来保存host值,这样原host就可以保存ip,这里保存时机host了。
if (_endpoint.origin.domainHost.length > 0) {
            NSMutableDictionary *newTlsSettings = [tlsOp->_tlsSettings mutableCopy];
            newTlsSettings[(__bridge NSString *)kCFStreamSSLPeerName] = _endpoint.origin.domainHost;
            tlsOp->_tlsSettings = newTlsSettings;
        }

spdy里http转https的一些主要代码。

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
+ (BOOL)canInitWithRequest:(NSURLRequest *)request
{
    // 由于我们做的是将http转成https,所以就不怕行程循环触发protocol
    if ([request.URL.scheme isEqualToString:@"http"]) {
        return YES;
    }
    return NO;
}


+ (NSURLRequest *)canonicalRequestForRequest:(NSURLRequest *)request
{
    return [self tryGetHttpsRequest:request];
}

- (NSURLRequest *)tryGetHttpsRequest:(NSURLRequest *)oldReq
{
    NSString *oldScheme = [[oldReq.URL scheme] lowercaseString];
    
    if ([oldScheme isEqualToString:@"http"] && [self shouldInterceptorRequest:oldReq]) {
        NSMutableURLRequest *newReq = [oldReq mutableCopy];
        NSURL *newUrl = [[NSURL alloc] initWithScheme:@"https" host:oldReq.URL.host path:oldReq.URL.path];
        newReq.URL = newUrl;
        // 下面这段代码是因为现有的有spdy服务器对于strem header里中文直接断开连接,而cocoaspdy里获取的是应用的名称,如果你的名称正好是中文,会导致steam一发送就被rst,所以我们主动去创建一个host,这样spdy就不会用内部的defaultuseragent来覆盖
        if (![newReq valueForHTTPHeaderField:@"User-Agent"]) {
            NSMutableDictionary *allHTTPHeaderFields = [newReq.allHTTPHeaderFields mutableCopy];
            [allHTTPHeaderFields setObject:[self getDefaultUserAgent] forKey:@"User-Agent"];
            newReq.allHTTPHeaderFields = allHTTPHeaderFields;
        }
        return newReq;
    }
    
    return oldReq;
}

- (instancetype)initWithHosts:(NSArray *)hosts
{
    self = [super init];
    if (self)
    {
      //拦截HTTP,并且host为指定的host
        _predicate = [NSPredicate predicateWithFormat:@"scheme MATCHES 'http' AND host IN[cd] %@", hosts];
        _hostPredicate = [NSPredicate predicateWithFormat:@"SELF IN[cd] %@", hosts];
    }
    
    return self;
}

- (BOOL)shouldInterceptorRequest:(NSURLRequest *)request
{
    if ([self.predicate evaluateWithObject:request.URL])
    {
        return YES;
    }
    
    // 这个是针对httpdns之后,host已经转到request里的host字段里了,所以做二次判断。
    if ([self isHeaderHostVaild:request]) {
        return YES;
    }
    
    return NO;
}

- (BOOL)isHeaderHostVaild:(NSURLRequest *)request
{
    NSString *hostInHeader = [request valueForHTTPHeaderField:@"Host"];
    if (hostInHeader && [self.hostPredicate evaluateWithObject:hostInHeader]) {
        return YES;
    }
    return NO;
}

One more thing

当你完成http转httpdns,转https之后,你可能碰到sdwebimage在滚出页面的时候,会去cancel request,但是走spdy之后,它会触发protocol的stopLoading方法,此时需要关注这个stopLoading触发的时候,此前用于2次转发的NSUrlConnection对象我们需要判断handler是否可用,如果可用,说明并不是正常结束,而是被cancel了,此时应该执行[connectoin cancel]操作。

关于ATS

这里简单说明下ATS,不细说,详情各位看官自己google下,一大堆资料,也可以查看这里.

iOS9适配

iOS9适配

提交iOS app的时候,踩了一遍开发都需要踩的坑,虽然其他地方都能找到,这里还是说下自己的坑

一、App Transport Security

xcode7安装后,你会发现ios9之后后默认所有http请求都无法继续有效,但是基于现状,我们并不能这么快改成https请求,所以基本上大多数app都会选择兼容老模式

如果服务不改变,则客户端info.plist的根需加下面的键值(这些key可以手动在project的info里直接添加和修改)

简单信任所有http服务器

1
2
3
4
5
<key>NSAppTransportSecurity</key>
<dict>
      <key>NSAllowsArbitraryLoads</key> 
  <true/>
</dict> 

或者另外严谨一些

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<key>NSAppTransportSecurity</key>
<dict>  
  <key>NSExceptionDomains</key>  
      <dict>    
          <key>yourserver.com</key>    
              <dict>      
              <!--Include to allow subdomains-->      
              <key>NSIncludesSubdomains</key>
                  <true/>
              <!--Include to allow insecure HTTP requests-->                    <key>NSTemporaryExceptionAllowsInsecureHTTPLoads</key>     
                  <true/>
              <!--Include to specify minimum TLS version-->      
              <key>NSTemporaryExceptionMinimumTLSVersion</key>      
                  <string>TLSv1.2</string>    
              <!--whether domain support forward secrecy using ciphers, if not support, set false-->
              <key>NSExceptionRequiresForwardSecrecy</key>
                  <false/>
              </dict>  
      </dict>
</dict> 

另外由于苹果的https的限制(注意理论上苹果ATS生效的是调用NSURLConnection, CFURL, or NSURLSession APIs的所有连接),还不仅仅限于任何https,还必须满足一定的其他要求,比如加密算法的要求,TLS的协议版本等。(详情查看https://developer.apple.com/library/prerelease/ios/technotes/App-Transport-Security-Technote/)

比如网上有人分析了百度的https就不能满足默认的苹果https安全传输要求,因为它的TLS虽然满足TLS1.2,但是加密算法是:SHA-1 with RSA Encryption ,所以依然会被报警,所以,这种exception的情况也需要额外标注,上面的官方连接也有说明,即添加NSExceptionRequiresForwardSecrecy,并设置为false,禁止forword secrecy。

二、bitcode

xcode7 默认开启,bitcode(iwatch需要),则会导致部分第三方框架报错(比如友盟的错误)

libMobClickLibrary.a(MobClick.o)‘ does not contain bitcode. You must rebuild it with bitcode enabled (Xcode setting ENABLE_BITCODE), obtain an updated library from the vendor, or disable bitcode for this target. for architecture armv7

这是要么更新库,要么可以在 build setting 中,搜索bitcode,并把enable bitcode 设置为 NO,这个因各自app情况而定,实际上本身不会降低上传包大小,只是开启后能降低用户下载的大小。苹果会自动根据用户自身的设备选择相关架构的下载

三、iOS9安装企业证书打包的app

企业证书打包的app,安装到手机里面后第一次打开app。不会像以前一样自动提示,信任还是不信任该证书;

这是个时候需要iOS9 设置-》通用-》描述文件-》企业级应用 中信任对应的企业开发者。

四、iOS9 URL Schemes

苹果新上线urlScheme的限制

1
2
3
4
5
If you call the “canOpenURL” method on a URL that is not in your whitelist, 
  it will return “NO”, even if there is an app installed that has registered to handle this scheme. A “This app is not allowed to query for scheme xxx” syslog entry will appear.

If you call the “openURL” method on a URL that is not in your whitelist, 
  it will fail silently. A “This app is not allowed to query for scheme xxx” syslog entry will appear.

更多信息:WWDC 2015 Session 703: “Privacy and Your App” https://developer.apple.com/videos/wwdc/2015/?id=703

因此现在要搞分享的时候,除了要在项目info URL Types中设置URL Schemes,还需要在info.plist里面增加可信任的调用app url scheme,否则回报如下错误

-canOpenURL: failed for URL: “weixin://app/wx********/” - error: “This app is not allowed to query for scheme weixin”

只需要添加如下代码即可,在info.plist里加入

1
2
3
4
5
6
7
<key>LSApplicationQueriesSchemes</key>
  <array>
  <string>weixin</string>
  <string>wechat</string>
  <string>sina</string>
  <string>weibo</string>
</array>

你也可以查看友盟分享SDK适配iOS9的文档:http://dev.umeng.com/social/ios/ios9

未完待续

一个pod Update遇到的错误处理

pod update的时候生成xcworkspace后,link的时候碰到了一个问题,就是一些pod里的库link不到,当然此时你可以跑到工程里主动添加link lib也是可以解决问题,不过那是治标不治本,实际上的处理应该是:

  1. 观察pod update的时候的提示,比如我这个是有如下提示:

     target overrides the `OTHER_LDFLAGS` build setting defined in `Pods/Target Support Files/Pods/Pods.debug.xcconfig'. This can lead to problems with the CocoaPods installation
    

    此时很明显说明pod生成的xcconfig文件里关于OTHER_LDFLAGS的设置和工程里的build setting冲突了,最后target的设置覆写了xcconfig里的设置。而你要知道工程之所以能找到pod里的link的库,就是因为这个xcconfig在起作用,现在被覆盖了,自然无效了。

    所以为了解决这个问题,你应该首先先尝试删除工程里的build setting的Other linker flags里一些可能冲突的参数设置,然后添加 $(inherited) 进去。重新执行pod update即可解决问题

Bitbucket

完美的私人仓库

一直想弄个私人仓库,最好无限制创建私人仓库。最好还能有个team可以共享访问私人仓库。本来差点就要去自己搭建一个gitlab了,然后突然看到bitbucket,发现原来完美的私人仓库就是他了。

果断申请账号(必须是免费的5人组team形势的),然后折腾一番将自己的一些小代码放上去。 中间碰到了一些问题,以为类似github上的ssh key的添加,结果添加ssh key到repo里的一个deployment key去了(这个应该是用来专门部署的时候用的key,只读性质的)。 查了下,发现overflow上有人说明了原因,果断尝试了OK解决问题,看客们自己也试试吧,值得推荐!

http://stackoverflow.com/questions/13306435/repository-access-denied-access-via-a-deployment-key-is-read-only/13309843#13309843

1
2
3
4
5
6
7
8
9
First confusion on my side was about where exactly to set SSH Keys in BitBucket.

I am new to BitBucket and I was setting a Deployment Key which gives read-access only.

So make sure you are setting your rsa pub key in your BitBucket Account Settings.

Click your BitBucket avatar and select Manage account. There you'll be able to set SSH Keys.

I simply deleted the Deployment Key, I don't need any for now. And it worked

发布自己的github上的cocoaPods Pod库

今天第一次在github上publish一个ILEditLabel控件,同时支持了cocoapods查找该pod库。

这里简单说下几个小坑。

提交前注意给你的github的代码打tag,比如这里的0.0.1,然后编写podspec文件,对应好里面的source的tag,其实可以将pod的tag和你的库的tag一致,这样方便记忆。

然后你需要执行pod spec lint specFile测试下是否spec文件编写正确。

如果发现远程tag打错了,你可以通过命令git push origin refs/tags/yourtag来删除远程tag。

通过podspec文件的检测后,就到了发布该spec文件了。

那就是需要通过pod trunk push specFile的方式来提交spec文件到cocoapods上。

但是由于我是第一次提交,执行命令后提示:

1
You need to register a session first.

所以需要参照cocoapods上的这篇文章Getting setup with Trunk先注册自己的session。

注册需要执行如下命令:

1
pod trunk register orta@cocoapods.org 'Orta Therox' --description='macbook air'

然后你就会在你输入的orta@cocoapods.org邮箱里收到一个email,点击里面的连接完成认证,之后你就可以完成提交。至此自己故障庆祝吧~~~