Adailly blog

记录这件小事...


  • 首页

  • 标签

  • 归档

iOS单例的创建和继承

发表于 2018-09-12

单例

一般来说,在iOS的工程中, 通用的tool一般都会设计成单例,这样能保证在程序运行过程中,程序中只有一个单例对象,方便数据的传输和处理。
如果为了扩展此单例类,希望在此基础上增加面向各模块专用的单例方法,该如何处理呢。
可以采用的方法有两种:

  • 分类:可以针对各个模块创建对应的分类,并添加方法。缺点是不能继承原有单例的属性。
  • 继承:也可以针对各个模块创建出对应的子类,并添加方法。也可以继承父类的属性。

继承单例遇到的问题

因为单例对象在程序中只会创建一次,创建成功后再次访问单例对象,访问的是同一个对象,故子类如果采用父类的方法进行创建的话,访问的对象还是父类,子类添加的属性和方法将会无法访问而造成crash。

1
2
3
4
5
6
7
8
9
10
// 父类
QYSingleton *singleton = [QYSingleton shareSingleton];
NSLog(@"%@ - %p", singleton, singleton);
// 方法
[singleton run];

// 打印结果

单例继承[3651:162046] name: father, age: 30, sex: male - 0x604000221da0
单例继承[3711:165295] QYSingleton run
1
2
3
4
5
6
7
// 子类
// 子类:采用继承的父类的类方法进行创建
QYSubSingleton *subSingleton = [QYSubSingleton shareSingleton];
NSLog(@"%@ - %p", subSingleton, subSingleton);

// 打印结果
name: father, age: 30, sex: male - 0x604000221da0

由以上结果可知,这两个对象的地址是一样的,也证明了程序中只会创建一份单例对象。

  • 给子类对象添加属性,因创建出来的对象为父类对象,故-[QYSingleton setHobby:]: unrecognized selector sent to instance 0x60400002b8a0找不到属性对应的setter而crash.
  • 给子类对象添加方法,同样的会-[QYSingleton walk]: unrecognized selector sent to instance 0x6000002212e0

子类的创建方法

  • 通过alloc、init进行创建
    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
    // 父类
    QYSingleton *singleton = [QYSingleton shareSingleton];
    NSLog(@"%@ - %p", singleton, singleton);
    // 方法
    [singleton run];

    // 打印结果
    单例继承[6181:308541] name: father, age: 30, sex: male - 0x6040000349e0
    单例继承[6220:310333] QYSingleton run

    // 通过alloc、init创建子类
    QYSubSingleton *subSingleton = [[QYSubSingleton alloc] init];
    // 继承的属性
    subSingleton.name = @"subObject";
    // 子类添加的属性
    subSingleton.hobby = @"游泳";
    NSLog(@"%@ - %p", subSingleton, subSingleton);

    // 继承的方法
    [subSingleton run];
    // 添加的方法
    [subSingleton walk];

    // 打印的结果
    单例继承[6220:310333] name: subObject, age: 30, sex: male, hobby:游泳 - 0x60000025ad90
    // 调用父类的方法
    单例继承[6220:310333] QYSubSingleton run
    // 子类添加的方法
    单例继承[6220:310333] QYSubSingleton walk

由打印可知,这样才能创建出新的子类对象,并且可以更改父类的初始属性,添加新的属性和方法

  • 给子类添加单例的方法进行创建
    • 方法:+ (instancetype)shareSubSingle;
    • 使用:QYSubSingleton *subSingleton = [QYSubSingleton shareSubSingle];
    • 验证:将alloc、init替换为shareSubSingle创建子类对象进行验证
    • 结论:经验证,这样也能创建出新的子类对象,并能添加属性和方法

更严谨的创建单例的方式

以上创建单例和单例的子类都不是很严谨,父类通过shareSingleton创建的对象,和通过alloc、init创建的对象是不一样的。
这是因为,OC通过allocWithZone:这个方法来申请内存的,故我们要覆写这个方法,保证创建出来的对象是唯一的。

1
2
3
+ (instancetype)allocWithZone:(struct _NSZone *)zone {
return [QYSingleton shareSingleton];
}

修改shareSingleton为:

1
2
3
4
5
6
7
8
+ (instancetype)shareSingleton {
static dispatch_once_t onceToken;
static QYSingleton *singleton = nil;
dispatch_once(&onceToken, ^{
singleton = [[super allocWithZone:NULL] init];
});
return singleton;
}

这样就不管是使用什么方法,创建出来的对象就都是唯一的了。

创建继承的单例

因为单例具有唯一性,要保证每个创建的类是不同的,所以在每个类生成时,我们动态的给类绑定唯一的对象。
使用runTime的关联对象进行创建。
如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
/** 单例对象 */
+ (instancetype)shareObject {
// 获取当前对象的类
Class selfClass = [self class];
// 从类中获取对象
id instance = objc_getAssociatedObject(selfClass, @"shareObject");
if (!instance) {
// 不存在,创建对象
instance = [[super allocWithZone:NULL] init];
// 给类绑定对象
objc_setAssociatedObject(selfClass, @"shareObject", instance, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
return instance;
}

/** 保证alloc、init也创建同样的对象 */
+ (instancetype)allocWithZone:(struct _NSZone *)zone {
// 获取当前对象的类
Class class = [self class];
// 返回当前类绑定的对象
return [class shareObject];
}

验证:

1
2
3
4
5
6
7
8
9
10
11
QYObject *objc = [QYObject shareObject];
NSLog(@"%p", objc);

QYObject *objc2 = [[QYObject alloc] init];
NSLog(@"%p", objc2);

QYSubObject *subObjc = [QYSubObject shareObject];
NSLog(@"%p", subObjc);

QYSubObject *subObjc2 = [[QYSubObject alloc] init];
NSLog(@"%p", subObjc2);

结果:

1
2
3
4
2018-09-12 09:43:22.764627+0800 单例继承[1639:49824] 0x6040000030d0
2018-09-12 09:43:22.764815+0800 单例继承[1639:49824] 0x6040000030d0
2018-09-12 09:43:22.764953+0800 单例继承[1639:49824] 0x604000002d90
2018-09-12 09:43:22.765140+0800 单例继承[1639:49824] 0x604000002d90

iOS系统加密函数详解:MD5、SHA、DES

发表于 2018-09-04

iOS系统加密函数详解

加密的种类

  • 单向加密
    通过对数据进行摘要计算生成密文,密文不可逆推还原,如:MD5、SHA、Base64
  • 双向加密
    与单向加密相反,可以把密文逆推还原成明文,双向加密又分为对称加密和非对称加密

    • 对称加密
      数据使用者必须拥有相同的秘钥才可以进行加密解密。如:DES、3DES、AES、IDEA、RC4、RC5
    • 非对称加密
      相对对称加密,无需拥有同一组密钥。非对称加密是一种“信息公开的密钥交换协议”。
      非对称加密需要公开密钥和私有密钥两组密钥,把公开的密钥为公钥,不公开的密钥为私钥。公钥和私钥是配对起来的,使用公钥进行数据加密,只有对应的私钥才能解密,这两个密钥是数学相关的,如果知道了其中一个,并不能计算出另外一个。因此如果公开了一对密钥中的一个,并不会危害到另外一个密钥的安全。
      如:RSA、DSA
  • iOS中系统封装好的加密函数有:MD5、SHA1~SHA512、DES等,使用系统加密函数,需要先引入头文件<CommonCrypto/>,其下有针对不同加密模式的分类,注意选择对应的模式。

  • 单项加密引入:#import <CommonCrypto/CommonDigest.h>
  • 双向加密引入:#import <CommonCrypto/CommonCrypto.h>

iOS系统加密函数的用法

MD5

MD5可以产生一个128位的散列值。8bit = 1byte,故生成16字节的散列值。
16进制两个字符等于一个字节,故生成的16进制密文为32个字符。
1byte可以表示的数字范围为0~255,刚好等于2个16进制字符的范围。

  • 需要用到的方法
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    /** 获取字符串编码后的长度
    * 也可以通过strlen([string UTF8String])获取
    */
    - (unsigned int)stringLength:(NSString *)string {
    return (unsigned int)[string lengthOfBytesUsingEncoding:NSUTF8StringEncoding];
    }

    /**
    将加密后的字符转为16进制

    @param data 生成的密文数据
    @param length 数据的长度
    @return 16进制的密文
    */
    - (NSString *)hexStringWithCipherTextData:(unsigned char *)data length:(unsigned int)length {
    // 开辟length*2的字节空间,16进制中2个字符为一个字节
    NSMutableString *hexStr = [NSMutableString stringWithCapacity:length * 2];
    for (int i = 0; i < length; i++) {
    [hexStr appendFormat:@"%02x", data[i]];
    }
    return hexStr;
    }

MD5示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
/**
* MD5加密后的密文长度为128bit,即16字节 = 16 * 8,转为16进制为32字符
* 16进制:2字符为一字节,16 * 2 = 32
*/
/** MD5加密 */
- (NSString *)MD5:(NSString *)string {
// 系统提供的密文长度:16字节
unsigned int outPutLength = CC_MD5_DIGEST_LENGTH;
// 获取明文编码后的长度
unsigned int inPutLength = [self stringLength:string];
// 根据密文的长度,创建一个保存密文的数组
unsigned char outPutData[outPutLength];
// 调用系统的方法
/*extern unsigned char *CC_MD5(const void *data, CC_LONG len, unsigned char *md)*/
CC_MD5([string UTF8String], inPutLength, outPutData);
// 返回16进制密文
return [self hexStringWithCipherTextData:outPutData length:outPutLength];
}

SHA SHA的调用和MD5类似

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
/** SHA加密后的字节大小
* CC_SHA1的字节长度20,对应的16进制字符数是20*2
* CC_SHA256的字节长度分别是32,对应的16进制字符数是32*2
* CC_SHA384的字节长度分别是48,对应的16进制字符数是48*2
* CC_SHA512的字节长度分别是64,对应的16进制字符数是64*2
*/
/** SHA1加密 */
- (NSString *)SHA1:(NSString *)string {
unsigned int outPutLength = CC_SHA1_DIGEST_LENGTH;
unsigned int inPutLength = [self stringLength:string];
unsigned char outPutData[outPutLength];

CC_MD5([string UTF8String], inPutLength, outPutData);

return [self hexStringWithCipherTextData:outPutData length:outPutLength];
}

/** SHA256加密 */
- (NSString *)SHA256:(NSString *)string {
unsigned int outPutLength = CC_SHA256_DIGEST_LENGTH;
unsigned int inPutLength = [self stringLength:string];
unsigned char outPutData[outPutLength];

CC_MD5([string UTF8String], inPutLength, outPutData);

return [self hexStringWithCipherTextData:outPutData length:outPutLength];
}

DES加密

概念

DES全称为Data Encryption Standard,即数据加密标准,是一种使用密钥加密的块算法,1977年被美国联邦政府的国家标准局确定为联邦资料处理标准(FIPS),并授权在非密级政府通信中使用,随后该算法在国际上广泛流传开来。需要注意的是,在某些文献中,作为算法的DES称为数据加密算法(Data Encryption Algorithm,DEA),已与作为标准的DES区分开来.

原则

DES设计中使用了分组密码设计的两个原则:

  • 混淆(confusion)
    混淆是使密文的统计特性与密钥的取值之间的关系尽可能复杂化,以使密钥和明文以及密文之间的依赖性对密码分析者来说是无法利用的。
  • 扩散(diffusion)
    扩散的作用就是将每一位明文的影响尽可能迅速地作用到较多的输出密文位中,以便在大量的密文中消除明文的统计结构,并且使每一位密钥的影响尽可能迅速地扩展到较多的密文位中,以防对密钥进行逐段破译。

  • 目的:抗击敌手对密码系统的统计分析。

加密模式

DES的加密模式有如下几种:

  • DES/CBC/PKCS5Padding 对应的OC模式:kCCOptionPKCS7Padding
  • DES/ECB/PKCS5Padding 对应的OC模式:kCCOptionPKCS7Padding | kCCOptionECBMode

每一段的解释如下:

  • 第一段:加密算法的名称
  • 第二段:分组加密模式,除了CBC和ECB之外,还可以是NONE/CFB/QFB等。最常用的就是CBC和ECB。
    DES采用分组加密的方式,将明文按8字节(64位)分组分别加密。如果每个组独立处理,则是ECB。
    CBC的处理方式是先用初始向量iv对第一组加密,再用第一组的密文座位秘钥对第二组加密,然后依次完成整个加密操作。
    如果明文中有两个分组的内容相同,ECB会得到完全一样的密文,CBC则不会。
  • 第三段:最后一个分组的填充方式。大部分情况下,明文并非刚好64位的倍数。对于最后一个分组,如果长度小于64位,则需要用数据填充至64位。
    PKCS5Padding是常用的填充方式,如果没有指定,默认的方式就是它。
    ps:DES的有效秘钥长度是56位,但要求秘钥长度是64位(8字节)。3DES则要求24字节。

加密函数CCCrypt详解

1
2
3
4
5
6
7
8
9
10
11
12
CCCryptorStatus CCCrypt(
CCOperation op, /* kCCEncrypt, etc. */
CCAlgorithm alg, /* kCCAlgorithmAES128, etc. */
CCOptions options, /* kCCOptionPKCS7Padding, etc. */
const void *key,
size_t keyLength,
const void *iv, /* optional initialization vector */
const void *dataIn, /* optional per op and alg */
size_t dataInLength,
void *dataOut, /* data RETURNED here */
size_t dataOutAvailable,
size_t *dataOutMoved);
  • CCCrypt:函数名
  • 摘要:无状态的, 一次加密或解密的操作。是对这些加密函数的封装:CCCrytorCreate(),CCCryptorUpdate(), CCCryptorFinal(), and CCCryptorRelease()
  • op:加解密,枚举值:kCCEncrypt,kCCDecrypt
  • alg:算法名称,kCCAlgorithmAES128,kCCAlgorithmAES,kCCAlgorithmDES,kCCAlgorithm3DES,kCCAlgorithmCAST,kCCAlgorithmRC4,kCCAlgorithmRC2, kCCAlgorithmBlowfish
  • options:填充方式,通常是kCCOptionPKCS7Padding,默认分组模式CBC。OC中提供两种模式:kCCOptionPKCS7Padding、kCCOptionECBMode
  • key:秘钥
  • keyLength:秘钥长度,必须和选择的算法相匹配,不同的算法要求的秘钥长度不一样。可选值如下:
    • kCCKeySizeAES128 = 16,
    • kCCKeySizeAES192 = 24,
    • kCCKeySizeAES256 = 32,
    • kCCKeySizeDES = 8,
    • kCCKeySize3DES = 24,
    • kCCKeySizeMinCAST = 5,
    • kCCKeySizeMaxCAST = 16,
    • kCCKeySizeMinRC4 = 1,
    • kCCKeySizeMaxRC4 = 512,
    • kCCKeySizeMinRC2 = 1,
    • kCCKeySizeMaxRC2 = 128,
    • kCCKeySizeMinBlowfish = 8,
    • kCCKeySizeMaxBlowfish
  • iv:加密使用的向量参数,CBC模式需要,16字节。ECB模式不需要。
    原始解释:初始向量,可选类型,用于CBC模式。如果存在,则必须与所选算法的块大小相同。如果选择了CBC模式(由于选项标志中没有任何模式位),并且没有IV,将使用NULL(所有0)IV。如果使用ECB模式或选择了流密码算法,则忽略此操作。对于声音加密,总是使用随机数据初始化IV。
    iv的创建有三种方式:

    • const Byte iv[] = {1,2,3,4,5,6,7,8};
    • const Byte iv[] = {0,1,2,3,4,5,6,7};
    • 1
      2
      3
      NSString *testString = key; // 秘钥
      NSData *testData = [testString dataUsingEncoding: NSUTF8StringEncoding];
      Byte *iv = (Byte *)[testData bytes];
  • dataIn:加解密的数据,const char *类型,使用字符串的UTF8String进行转换

  • dataInLength:数据的长度,类型size_t
  • dataOut:输出的数据,加密解密后的数据写在这里,
  • dataOutAvailable:输出数据时需要的可用空间大小。数据缓冲区的大小(字节)
  • dataOutMoved:输出数据实际的大小。返回成功后,写入dataOut的字节数。如果由于提供的缓冲区空间不足而返回kCCBufferTooSmall,则在这里返回所需的缓冲区空间。
  • result:返回的结果
    • kCCBufferTooSmall: 指示dataOut缓冲区中不充足的空间。在本例中,*dataOutMoved参数将指示完成操作所需的缓冲区大小。操作可以重试,运行时损失最小。
    • kCCAlignmentError: 指示数据长度没有正确对齐。这只能对块加密返回,然后只有在解密或加密时禁用填充块时才能返回。
    • kCCDecodeError : 指示格式不正确的密文或“错误的键”错误;仅在解密操作期间发生。
    • kCCSuccess = 0,
    • kCCParamError = -4300,
    • kCCMemoryFailure = -4302,
    • kCCUnimplemented = -4305,
    • kCCOverflow = -4306,
    • kCCRNGFailure = -4307,
    • kCCUnspecifiedError = -4308,
    • kCCCallSequenceError= -4309,
    • kCCKeySizeError

CCCrypt调用示例

  • 加密

    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
    //加密
    - (NSString *)encryptUseDES:(NSString *)plainText key:(NSString *)key {
    // 密文
    NSString *ciphertext = nil;

    // 加密后的数据
    uint8_t *dataOut = NULL;
    size_t dataOutAvailable = 0;
    size_t dataOutMove = 0;

    dataOutAvailable = (plainText.length + kCCBlockSizeDES) & ~(kCCBlockSizeDES - 1);
    dataOut = malloc(dataOutAvailable * sizeof(uint8_t));
    // 将已开辟内存空间buffer的首1个字节的值设为0
    memset((void *)dataOut, 0x0, dataOutAvailable);

    CCCryptorStatus cryptStatus = CCCrypt(kCCEncrypt, // 操作名称:加密
    kCCAlgorithmDES, // 加密算法
    kCCOptionPKCS7Padding | kCCOptionECBMode, // 填充模式,ECB模式
    key.UTF8String, // 加密秘钥
    kCCKeySizeDES, // 秘钥大小,和加密算法一致
    NULL, // 初始向量:ECB模式为空
    plainText.UTF8String, // 加密的明文
    (size_t)plainText.length, // 加密明文的大小
    dataOut, // 密文的接受者
    dataOutAvailable, // 预计密文的大小
    &dataOutMove); // 加密后密文的实际大小
    if (cryptStatus == kCCSuccess) {
    NSData *data = [NSData dataWithBytes:dataOut length:(NSUInteger)dataOutMove];
    // 将data转为16进制字符串
    ciphertext = [self convertDataToHexStr:data];
    }
    return ciphertext;
    }
  • 解密

    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
    //解密
    - (NSString *)decryptUseDES:(NSString*)cipherText key:(NSString*)key {
    // 将16进制转为data
    NSData* cipherData =[self convertHexStrToData:cipherText];

    // 解密后的数据
    uint8_t *dataOut = NULL;
    size_t dataOutAvailable = 0;
    size_t dataOutMove = 0;

    dataOutAvailable = (cipherData.length + kCCBlockSizeDES) & ~(kCCBlockSizeDES - 1);
    dataOut = malloc(dataOutAvailable * sizeof(uint8_t));
    // 将已开辟内存空间buffer的首1个字节的值设为0
    memset((void *)dataOut, 0x0, dataOutAvailable);

    CCCryptorStatus cryptStatus = CCCrypt(kCCDecrypt,
    kCCAlgorithmDES,
    kCCOptionPKCS7Padding | kCCOptionECBMode,
    key.UTF8String,
    kCCKeySizeDES,
    NULL,
    cipherData.bytes,
    (size_t)cipherData.length,
    dataOut,
    dataOutAvailable,
    &dataOutMove);
    NSString* plainText = nil;
    if (cryptStatus == kCCSuccess) {
    NSData* data = [NSData dataWithBytes:dataOut length:(NSUInteger)dataOutMove];
    plainText = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
    }

    return plainText;
    }
  • 16进制和NSData相互转换

    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
    //将NSData转成16进制
    - (NSString *)convertDataToHexStr:(NSData *)data {
    if (!data || [data length] == 0) {
    return @"";
    }
    NSMutableString *string = [[NSMutableString alloc] init];

    [data enumerateByteRangesUsingBlock:^(const void *bytes, NSRange byteRange, BOOL *stop) {
    unsigned char *dataBytes = (unsigned char*)bytes;
    for (NSInteger i = 0; i < byteRange.length; i++) {
    NSString *hexStr = [NSString stringWithFormat:@"%02x", (dataBytes[i]) & 0xff];
    [string appendString:hexStr];
    }
    }];
    return string;
    }

    //将16进制字符串转成NSData
    - (NSData *)convertHexStrToData:(NSString *)str {
    if (!str || [str length] == 0) {
    return nil;
    }

    NSMutableData *hexData = [[NSMutableData alloc] init];
    NSRange range;
    if ([str length] % 2 == 0) {
    range = NSMakeRange(0, 2);
    } else {
    range = NSMakeRange(0, 1);
    }
    for (NSInteger i = range.location; i < [str length]; i += 2) {
    unsigned int anInt;
    NSString *hexCharStr = [str substringWithRange:range];
    // 扫描字符串
    NSScanner *scanner = [[NSScanner alloc] initWithString:hexCharStr];
    [scanner scanHexInt:&anInt];

    NSData *entity = [[NSData alloc] initWithBytes:&anInt length:1];
    [hexData appendData:entity];

    range.location += range.length;
    range.length = 2;
    }
    return hexData;
    }

iOS网络层设计

发表于 2018-08-31

网络层设计

iOS项目架构

iOS的项目架构我一般设置为三层,即UI层、服务层、数据层,以及第三方依赖层。

  • UI层:主要管理项目中的所有UI界面,根据模块进行划分,如:登录模块,首页模块,搜索模块等等,还包括通用控件模块。
  • 服务层:基于各模块创建的服务于各模块的服务类,各模块的服务类之间相互独立,如果存在相互调用的情况,就将此抽象出来,设置到数据层中。UI层只负责UI的渲染和交互,数据之间的逻辑交由其对应的服务层处理。
  • 数据层:数据层提供基于整个项目最基础的一些工具,如网络基础工具,数据存取工具等等,一般在服务层中会针对各模块的实际需求进行二次封装和优化。

以上,数据层作为最基础的工具被各模块的服务层引用,各模块被对应的UI层应用,服务层之间和UI层之间相互独立,尽量解耦。

网络工具

了解了基本的项目架构后,就来详细说说数据层中的网络基础工具层的封装和服务层中网络工具使用,以及项目实际开发过程中遇到的难点及解决方法。

封装基础网络工具

iOS项目一般都会依赖AFNetworking网络库。
基础网络工具一般封装为单例模式,并根据项目需求,提供最基础的网络方法:GET、POST、上传方法(一般为上传图片),网络监听方法等。

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
#import <Foundation/Foundation.h>
#import <AFNetworking/AFNetworking.h>

/** 给子类使用的回调 */
typedef void(^netSuccessBlock)(id response);
typedef void(^netFailureBlock)(NSError *error);

@interface ZORNetworking : NSObject

/** 成功回调 */
@property (nonatomic, copy) netSuccessBlock netSuccessBlock;
/** 失败回调 */
@property (nonatomic, copy) netFailureBlock netFailureBlock;

/** 创建单例对象 */
+ (instancetype)shareNetwork;

/** 销毁单例对象 */
+ (void)destoryNetwork;

/** 添加请求头 */
- (void)addHeaderWithToken:(NSString *)token;

/** 监听网络状态 */
- (void)startMonitoringNetworkStatus;

/** 通知监听网络状态 */
- (void)stopMonitoringNetworkStatus;

/** GET方法 */
- (void)GET:(NSString *)URLString parameters:(id)parameters success:(void (^)(NSURLSessionDataTask *task, id responseObject))success
failure:(void (^)(NSURLSessionDataTask *task, NSError *error))failure;

/** POST方法 */
- (void)POST:(NSString *)URLString parameters:(id)parameters success:(void (^)(NSURLSessionDataTask *task, id responseObject))success
failure:(void (^)(NSURLSessionDataTask *task, NSError *error))failure;

@end

BaseUrl设置

网络请求工具中只要切换BaseURL就能自由访问不同的服务器,AFN提供了创建BaseURL的方法,在创建单例的时候自由创建。
初始化设置的时候根据后台的设置,进行AFN请求数据格式和接受数据格式的设置,AFN提供了两个类进行设置,self.manager.requestSerializer = [AFHTTPRequestSerializer serializer];, self.manager.responseSerializer = [AFHTTPResponseSerializer serializer];
具体参考AFHTTPResponseSerializer, AFHTTPRequestSerializer这两个类,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
+ (instancetype)shareNetwork {
static ZORNetworking *network = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
network = [[ZORNetworking alloc] init];
});
return network;
}

// 初始化方法
- (instancetype)init {
if (self = [super init]) {
self.manager = [[AFHTTPSessionManager alloc] initWithBaseURL:[NSURL URLWithString:BASE_URL]];
/** 请求设置 */
// 超时时间
self.manager.requestSerializer.timeoutInterval = 45.0;

/** 接收设置 */
// 返回值格式设置
self.manager.responseSerializer = [AFJSONResponseSerializer serializer];
self.manager.responseSerializer.acceptableContentTypes = [NSSet setWithObjects:@"application/json", @"text/json", @"text/javascript", @"text/html", nil];
}
return self;
}

提供销毁单例的方法

提供销毁单例的方法是因为,想在登录的时候自由切换请求的IP地址,即BaseURL,退出登录后重新登录网络请求的单例类仍然存在,并不会重新创建,所以无法在不进行commond+R的情况下自由切换。
提供销毁单例的方法后,就可以自由的切换BaseURL,切换后再次调用网络方法,将会创建一个新的网络单例类。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 将静态变量设置为全局可访问
static ZORNetworking *network = nil;
static dispatch_once_t onceToken;

@implementation ZORNetworking

+ (instancetype)shareNetwork {
dispatch_once(&onceToken, ^{
network = [[ZORNetworking alloc] init];
});
return network;
}

+ (void)destoryNetwork {
// dispatch_once_t 为 long 类型
onceToken = 0l;
network = nil;
}

@end

创建基础的请求方法

以上基础设置完成后,就可以创建基础的请求方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
- (void)GET:(NSString *)URLString parameters:(id)parameters success:(void (^)(NSURLSessionDataTask *, id))success failure:(void (^)(NSURLSessionDataTask *, NSError *))failure {
[self.manager GET:URLString parameters:parameters progress:nil success:^(NSURLSessionDataTask * _Nonnull task, id _Nullable responseObject) {
if (success) {
success(task, responseObject);
}
} failure:^(NSURLSessionDataTask * _Nullable task, NSError * _Nonnull error) {
if (failure) {
failure(task, error);
}
}];
}

- (void)POST:(NSString *)URLString parameters:(id)parameters success:(void (^)(NSURLSessionDataTask *, id))success failure:(void (^)(NSURLSessionDataTask *, NSError *))failure {
[self.manager POST:URLString parameters:parameters progress:nil success:^(NSURLSessionDataTask * _Nonnull task, id _Nullable responseObject) {
if (success) {
success(task, responseObject);
}
} failure:^(NSURLSessionDataTask * _Nullable task, NSError * _Nonnull error) {
if (failure) {
failure(task, error);
}
}];
}

网络监控方法的创建和调用

AFN提供了监控网络状态的方法,可以直接调用使用,为了实时获取网络的状态,一般在APP的启动方法里调用,但是如果网络状态的枚举类型定义在了网络工具单例类中,并定义相关的枚举属性,虽然可以实时监测到,但是通过状态属性并不能实时获取到。需要创建新的网络状态的单例类来获取,在需要检测网络状态的地方调用即可。
检测网络状态方法如下:

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
- (void)startMonitoringNetworkStatus {
[[self.manager reachabilityManager] setReachabilityStatusChangeBlock:^(AFNetworkReachabilityStatus status) {
switch (status) {
case AFNetworkReachabilityStatusReachableViaWiFi: {
[ZORNetworkStatus shareNetStatus].networkStatus = NetworkStatusViaWiFi;
DLog(@"wifi");
break;
}
case AFNetworkReachabilityStatusReachableViaWWAN: {
[ZORNetworkStatus shareNetStatus].networkStatus = NetworkStatusViaWWAN;
DLog(@"移动网络");
break;
}
case AFNetworkReachabilityStatusNotReachable: {
[ZORNetworkStatus shareNetStatus].networkStatus = NetworkStatusNotReachable;
DLog(@"无网络");
break;
}
case AFNetworkReachabilityStatusUnknown: {
[ZORNetworkStatus shareNetStatus].networkStatus = NetworkStatusUnKnown;
DLog(@"未知网络");
break;
}
default:
break;
}
}];
[[self.manager reachabilityManager] startMonitoring];
}

网络服务层

通过继承或分类创建进一步解析后的网络服务层,在这层中根据返回的结果设置自定义的提示内容,如加载动画,加载完成提示,网络不佳提示,无数据提示,错误提示等等。

基础服务

创建基础网络服务,通过继承或分类进行创建,根据产品的设计,进行不同情况的一系列提示。
其中NSError为自定义的error

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
- (void)POST:(NSString *)URL params:(id)params success:(void (^)(id))success failure:(void (^)(NSError *))failure {
// 加载动画设置
// code

[self POST:URL parameters:params success:^(NSURLSessionDataTask *task, id responseObject) {
// 隐藏加载动画
// code

// 根据后台的通用数据结构,解析返回结果,
NSDictionary *response = responseObject;

// 结果中可能带有code和message,解析后进行不同的UI设置

// code解析
NSInteger ok = [response[@"code"] integerValue];
// 获取后台返回的消息
NSString *msg = response[@"message"];

if (成功) { // 有数据
// 解析出数据后返回,一般为json字符串
// code
if (success) {
success(data);
}
} else { // 失败
// 不同失败结果的UI设置
// 无数据设置等等
// code

if (failure) {
// 自定义error对象并回调
NSError *error = [NSError errorWithDomain:NSURLErrorDomain code:NetErrorTypeBackData userInfo:@{NSLocalizedDescriptionKey : msg}];
failure(error);
}
}
} failure:^(NSURLSessionDataTask *task, NSError *error) {
// 隐藏加载动画
// code

// 解析error
// 根据error的code,和后台的说明进行设置
// 1、请求失败设置
// code

// 2、请求超时设置
// code

...
// 返回的error,可以直接返回,也可以进行自定义后返回
// code
if (error) {
if (failure) {
failure(error);
}
}
}];
}

各模块的网络服务层

针对不同的模块,创建不同模块的网络服务层,通过分类创建。
给模块中所有需要用到网络请求的action都创建一个对应的方法,方便区分和维护。
在这一层中,尽量将网络请求的URL、参数包装、解析后的数据等操作都进行处理,UI层只管调用方法,调用成功后直接拿到需要的数据进行UI的渲染。
通过这种设计最大程度的对ViewController进行瘦身。

AFN单独传递字符串参数的设置

项目中用到DES加密,后台要求将加密后的字符串密文直接传递过去,在以往的经验中,所有的参数都是通过NSDictionary进行传递的。直接改变参数为字符串传递,返回错误的结果。
结果调试,后台一直报的错误是格式错误。
经过查找资料,和对HTTP请求的理解有关,AFN默认的请求是键值形式的字符串,即application/x-www-form-urlencoded。
AFN内部会将接收到的参数自动按照键值的形式进行组织后传递,所有会看到错误为:input string '=%' ,类似这种错误。
遇到这种问题可以尝试下面两种方法进行解决。

  • 解决方法1
    传递的参数为纯文本形式的字符串,设置请求头为[self.manager.requestSerializer setValue:@"text/plain" forHTTPHeaderField:@"Content-Type"];。
  • 解决方法2
    AFN提供了requestSerializer的扩展方法,直接创建一个自定义的requestSerializer,就不需要通过init创建requestSerializer了,AFN就不会将参数按照键值的形式传递了。
    也有可能还需要设置请求头,具体根据后台进行设置。
    1
    2
    3
    4
    // 自定义requestSerializer
    [self.manager.requestSerializer setQueryStringSerializationWithBlock:^NSString * _Nonnull(NSURLRequest * _Nonnull request, id _Nonnull parameters, NSError * _Nullable __autoreleasing * _Nullable error) {
    return parameters;
    }];

RunLoop

发表于 2018-03-28

RunLoop

基本概念

RunLoop的基本概念在这里不做介绍,网上的资料有很多。

线程和RunLoop

一个线程对应一个RunLoop,主线程的RunLoop在程序运行时时默认打开的。在子线程中的RunLoop如果不主动获取,那么它就不会产生。
RunLoop的创建时在第一次获取时,RunLoop的销毁是在线程结束。你只能在一个线程内部获取其RunLoop。

获取并启动RunLoop

  • Foundation框架下:
    获取当前线程的RunLoop:self.runLoop = [NSRunLoop currentRunLoop];
    启动线程:[self.runLoop run]
    退出线程:关闭timer或移除port。

  • CoreFoundation框架下:
    @property (nonatomic, assign) CFRunLoopRef runloop;
    获取当前线程:self.runloop = CFRunLoopGetCurrent();
    启动线程:CFRunLoopRun();
    退出线程:CFRunLoopStop(self.runloop)

timer

我们都知道,RunLoop是通过不断的循环来执行事件的,执行的事件有两种类型,一种的timer,一种是source。
使用NSTimer的时候,我们都习惯的将NSTimer添加到当前的RunLoop中,一般是在主线程中。

1
2
NSTimer *timer = [NSTimer scheduledTimerWithTimeInterval:1.0 target:self selector:@selector(timeRunLoopAction) userInfo:nil repeats:YES];
[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];

这时currentRunLoop会一直循环执行,知道我们把timer关闭后,RunLoop因没有timer而退出。

1
2
[self.timer invalidate];
self.timer = nil;

常驻线程

如果我们需要开辟的子线程一直在后台运行,如在后台上传用户的相关数据,就需要开启当前线程的RunLoop。

CocoaPods安装

发表于 2018-03-22

CocoaPods安装

检查及安装rvm

指令:rvm -v,如没有进行安装rvm。

  • 安装rvm:curl -L get.rvm.io | bash -s stable
  • 指定源:source ~/.rvm/scripts/rvm
  • 查看是否安装成功:rvm -v
  • 列出指定源里面ruby的版本:rvm list known

安装ruby

在上一步中找出最新的ruby的版本。

  • 安装ruby:rvm install 2.4.1,根据提示按enter键即可,2.4.1是当前的最新版本。
  • 检查是否安装成功:rvm list

检查更新RubyGems

RubyGems是一个方便而强大的Ruby程序包管理器,Ruby的第三方插件是用gem方式来管理的,非常容易发布和共享,一个简单的命令就可以安装上第三方的扩展库。

  • 查看RubyGems的版本:gem -v
  • 更换源:gem sources --add https://gems.ruby-china.org/ --remove https://rubygems.org/
    因为墙的存在,需要更换Ruby的软件源rubygems.org,之前国内使用的 https://ruby.taobao.org,不再建议使用的了,这是因为 taobao Gems 源已停止维护,现由 ruby-china 提供镜像服务。
  • 查看源路径是否替换成功:gem sources -l
    确保只有 gems.ruby-china.org
  • 更新到最新版本:gem update --system
  • 查看版本:gem --version

安装CocoaPods

  • 安装指令:sudo gem install cocoapods
    OS X 10.11之前系统的安装cocoapods 指令: sudo gem install cocoapods
    OS X 10.11以后系统的安装cocoapods 指令: sudo gem install -n /usr/local/bin cocoa pods

  • 如果安装了多个Xcode使用下面的命令选择(最好选择最近的Xcode版本)
    sudo xcode-select -switch /Applications/Xcode.app/Contents/Developer

  • 同步CocoaPods:pod setup 这一步需要等待很长的时间,更新完毕就可以使用了。

iOS面试

发表于 2018-03-22

iOS面试

进入2018.3后开始了一波面试,现就总结下遇到的公司和面试的经验。
离开上一家公司

非技术问题

介绍自己

  • 介绍自己
  • 谈下项目经验

评价上一家公司

  • 为什么离职
  • 离职原因

职业规划

  • 看重什么样的公司
  • 对公司有什么要求
  • 对今后有什么规划

公司可能面临的问题

  • 怎么看待加班
  • 设定一个场景问你怎么做
    比如一个人开发能胜任吗,工作中遇到问题怎么解决,就是说你可能没有同事进行交流,也没有技术大牛对你进行指导。

技术问题

基础技术问题

一般有工作年限的基本技术都会掌握

简历中罗列的技术点对你进行提问

常用第三方及原理

用过哪些第三方,说说原理。

复杂问题的综合考虑

教育

发表于 2018-03-21

青少年常见心理问题与对策 - 李玫瑾教授讲座笔记

这个讲座的视频时间有2.5小时,听了非常受用。在即将打算抚育小孩之前学习育儿之道,对孩子观念、性格的塑造以及和父母的关系都有很大的帮助。
以下是李教授从事心理犯罪研究的几点总结:

未成年人的问题是成年人造成的

孩子的每种心理或行为问题,都与父母的抚育方式有关。
举个例子:孩子出生后1到1岁半之前的抚育非常重要,这期间孩子由谁抚育,谁就具有孩子一生的控制力。所以我们经常会看到或听到这个孩子不听父母的话,只听他奶奶或姥姥的,这就是因为这个阶段父母没有抚育的原因,或者说是因为没有长期抚育。

未成年人的问题是滞后反应

12岁后发生的问题,源由在12岁之前。

这是因为12岁之前孩子是弱小的,他还不能明显的按照自己的意愿去做一些事情,所以这个阶段不容易看出问题。

等我们有教育经验时,我们就没有机会了

等我们学习到了经验,孩子就长大了,错过了孩子观念和性格形成的关键时期,就很难改变孩子的行为了。

心理发展有关键期

做好父母,也需要耐心与智慧。我们需要时刻观察孩子成长的阶段,并在各个阶段调整自己的教育方式。尤其是在成长的关键阶段,如14岁,16岁,18岁等。

心理成长和发展的时期分类

年龄 阶段
0-1 乳儿期
2-3 婴儿期
4-5 幼儿期
6-11 学龄初期
12-15 少年期
16-18 青年初期
19-25 青年中期
26-35 青年后期
36-60 中年期
60以上 老年期

18岁之前,是孩子观念、性格塑造的关键时期,这期间孩子会经历:情感发展、言语发展、社会性发展、认知、观念、性格、自我意识、独立性。
18岁之后,孩子的观念、性格、人格趋于稳定,如果前期没有培养好,这个阶段就很难矫正了。

情感发展

半岁出现的心理反应:认人,依恋
人与人之间的关系:需要、情感
情感是诸多心理发展的基础。
父母由此获得抚养孩子的心理资本,一岁半之前谁带孩子谁获得孩子的心理资本,谁具有孩子的控制力。

言语发展

爱的抚养,一定是唠叨,一定是有亲人在身旁的抚养,听的多了自然话就多了。

社会性发展

言语发展和社会性发展息息相关,社会性主要指爱不爱和人交流。

认知、观念、性格

12岁之前培养。
观念:看到了就形成了。
信念:明白了并坚持。

观念:来自父母的唠叨,父母的影响,父母的教育。
性格:与别人的有关的行为,就是社会行为,就是性格。一个人和别人有关的行为方式,就是性格。

6岁之前塑造青少年,防止出现问题

克制任性、防止压抑、学会控制、学会忍耐、防止自私、经历挫折

克制任性

一定要说不。
四个不要的训练:不要骂他,不要打他,不要说教,不要走开。

学会控制

通过诱惑的方式,让其学会控制自己。

学会忍耐

吃饭的忍耐和控制。

防止自私

管理别人,吃饭立规矩。

经历挫折

体能训练,如学游泳。

青春期的教育

早期教育缺陷的补救期。

青春期引起的变化

生理:身高、生理变化
心理:
身高引起的心理变化:自己的事情自己做,独立意识增强,自尊性增强。少唠叨。
没新意导致逆反
改变教育的方式:点到为止,不要坚持太长时间。

成瘾问题

内向会导致不愿意发展其社会性。
内向的改变:交朋友,拔出来。培养多兴趣。
成瘾是因为:感觉单一,人会痴迷。
解决成瘾问题:
对孩子的兴趣表示肯定,与他共同发展,和他有共同的语言。

性教育

初中:性的魅力
高中:人类性史话。

生命的教育

如何宝贵生命:

  • 健康
  • 自由,守法才能享受自由
  • 情义

Hello Hexo

发表于 2018-03-13

Hello

一直想做个自己的博客,很久以前做过一个基础版的,后面不知道什么原因就不了了之了。
现在有空,就抓紧时间自己做一个了。
主要是配置文件的问题,如下是一些常用的命令,记录于此方便查看使用。

Hexo文档
Next主题文档

安装Hexo

  • 前提:已安装node、Git
  • 利用node安装Hexo:npm install -g Hexo-cli
  • 如果npm安装速度较慢或失败,建议把npm改为cnpm

建站

  • $ hexo init <folder>,floder为自定义的项目名称
  • $ cd <folder>,打开项目
  • $ npm/cnpm init,执行init操作
  • 配置 _config.yml,配置相关文件

常用命令

  • 新建文章:$ hexo new [layout] <title>,如果没有设置 layout 的话,默认使用 _config.yml 中的 default_layout 参数代替。title:文章名称
  • 生成静态文件:$ hexo generate
    • 该命令有两个选项:
    • -d: deploy 文件生成后立即部署网站
    • -w: watch 监视文件变动
    • 简写: $ hexo g (选项)
  • 发表草稿:$ hexo publish [layout] <filename>
  • 启动服务器:$ hexo server
    • 该命令有三个选项:
    • -p: port 重设端口
    • -s: static 只使用静态文件
    • -l: log 启动日记记录,使用覆盖记录格式
    • 简写: $ hexo s
  • 部署网站:$ hexo deploy
    • 该命令有一个选项
    • -g: generate 部署之前预先生成静态文件
    • 简写:$ hexo d
  • 清楚缓存:$ hexo clean

  • 使用sh命令一键部署:执行命令sh ~/hexod.sh

  • 命令的编写:
    • 进入到~目录
    • 使用vim hexod.sh编写命令
    • 命令内容:hexo clean && hexo g -d

adailly

8 日志
2 标签
© 2018 adailly
由 Hexo 强力驱动
|
主题 — NexT.Pisces v5.1.4