0%

注意点

1、第一次生成对象时不能调用[self alloc],因为这个方法内部调用的allocWithZone方法会被重写。可以调用[super allocWithZone:nil]来解决这个问题。
2、必须要遵守NSCopying NSMutableCopying 者两个协议,重写copyWithZone mutableCopyWithZone这两个方法,不然外部调用 copy mutableCopy方法会崩溃。 方法返回self
3、重写allocWithZone这个方法。返回第一次生成的对象。

代码

1
2
3
4
5
6
7
8
//.h
#import <Foundation/Foundation.h>

@interface VCHSingleton : NSObject

+ (instancetype)sharedInstance;

@end
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
//.m
#import "VCHSingleton.h"

@interface VCHSingleton() <NSCopying, NSMutableCopying>

@end

@implementation VCHSingleton

+ (instancetype)sharedInstance {
static id instane = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
instane = [[super allocWithZone:nil] init];
});
return instane;
}

+ (instancetype)allocWithZone:(struct _NSZone *)zone {
return [self sharedInstance];
}

- (id)copyWithZone:(NSZone *)zone {
return self;
}

- (id)mutableCopyWithZone:(NSZone *)zone {
return self;
}

@end

调用

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
VCHSingleton *singleton0 = [VCHSingleton sharedInstance];
NSLog(@"singleton0 = %@",singleton0);

VCHSingleton *singleton1 = [VCHSingleton new];
NSLog(@"singleton1 = %@",singleton1);

VCHSingleton *singleton2 = [VCHSingleton alloc];
NSLog(@"singleton2 = %@",singleton2);

VCHSingleton *singleton3 = [[VCHSingleton alloc] init];
NSLog(@"singleton3 = %@",singleton3);

VCHSingleton *singleton4 = [singleton0 copy];
NSLog(@"singleton4 = %@",singleton4);

VCHSingleton *singleton5 = [singleton0 mutableCopy];
NSLog(@"singleton5 = %@",singleton5);

// 输出
// singleton0 = <VCHSingleton: 0x60000001d410>
// singleton1 = <VCHSingleton: 0x60000001d410>
// singleton2 = <VCHSingleton: 0x60000001d410>
// singleton3 = <VCHSingleton: 0x60000001d410>
// singleton4 = <VCHSingleton: 0x60000001d410>
// singleton5 = <VCHSingleton: 0x60000001d410>

需求

APP 需要获取当前手机的时区,并以GMT+8的格式传给服务器。

解决方案

经过搜索以及测试,发现三种方法可以获取到手机的当前时区,不过在后面发现其中一种方法是有问题的。下面就来解析下三种方法。

方法一(后来发现这种方法有问题)

1
2
3
[NSTimeZone resetSystemTimeZone];
NSTimeZone *timeZone = [NSTimeZone systemTimeZone];
NSLog(@"timeZoneString = %@",timeZone.abbreviation);

在本地测试没有问题,比如手机时间是东八区的时间,那么就打印出GMT+8,设置成其他地区的时间也正常。
但当时区设置为0时,打印出来的是GMT,而不是GMT+0,从这个角度看,这种方法似乎也不妥,还需要自己转换。

此外,经过大量的测试,这种方法在个别手机上获取到的时间格式不是GMT的格式。比如香港的个别手机获取到的时区是HKT,表示的是香港时间。同样每个地区都有当地时区的简称(参考:世界时区)。 那么说明这种方法已经不适合了。。。。

其实看看文档就会发现这种方法不适合目前的需求。

The abbreviation for the receiver, such as “EDT” (Eastern Daylight Time).

Read more »

需求

用 Cookie 来实现自动登录的功能。

Cookie 是由服务器生成的,会在首次登陆或者重新登录时,通过 HTTP 的 Respone Header 发送给客户端。客户端一般会保存这个 Cookie 并在下一次访问该服务器时携带。服务器收到 Cookie 时会先判断 Cookie 是否有效,如果无效则提示客户端需要重新登录。

在 iOS 系统中,使用 NSURLRequest 的请求会默认加上 Cookie。同时也会默认保存以及更新 Cookie 。
Cookie 一般会在使用账号密码登陆时由服务器返回回来,下次登录时会根据这个 Cookie 来决定是否需要重新登录。登陆后每一次请求也会携带这个 Cookie ,用来判断是否有异地登录(Cookie 失效)。

在 macOS 中的 Cookie 是共享的,而 iOS 中的 Cookie 则是单独使用的。

iOS 对 Cookie 的相关操作主要有两个类。NSHTTPCookieStorage 和 NSHTTPCookie 。下面介绍下这两个类

Read more »

序言

谓词 NSPredicate 的基本用法。

示例

比较运算符(> , < , >= , <=)

1
2
3
4
// NSPredicate *predicate = [NSPredicate predicateWithFormat:@"self.age == 1"];
// NSPredicate *predicate = [NSPredicate predicateWithFormat:@"self.age >= 11"];
NSPredicate *predicate = [NSPredicate predicateWithFormat:@"self.age < 5"];
NSArray *filterArray = [arrays filteredArrayUsingPredicate:predicate];

逻辑运算符(and[&&] , or[||] , not[!])

1
2
3
//NSPredicate *predicate = [NSPredicate predicateWithFormat:@"(self.age < 5) or (self.age > 8)"];
NSPredicate *predicate = [NSPredicate predicateWithFormat:@"not (self.age == 5)"];
NSArray *filterArray = [arrays filteredArrayUsingPredicate:predicate];

范围运算符(in , between)

1
2
3
// NSPredicate *predicate = [NSPredicate predicateWithFormat:@"self.age between {2,5}"];
NSPredicate *predicate = [NSPredicate predicateWithFormat:@"self.age in {2,3}"];
NSArray *filterArray = [arrays filteredArrayUsingPredicate:predicate];

包含字符串(beginswith , endswith , contains)

1
2
3
4
//NSPredicate *predicate = [NSPredicate predicateWithFormat:@"self.name beginswith[c] 'vch'"];
//NSPredicate *predicate = [NSPredicate predicateWithFormat:@"self.name endswith[d] 'chan'"];
NSPredicate *predicate = [NSPredicate predicateWithFormat:@"self.name contains 'an'"];
NSArray *filterArray = [arrays filteredArrayUsingPredicate:predicate];

[c] 不区分大小写,[d] 不区分发音符号,[cd] 不区分大小写和发音符号

Read more »

需求

UITableView 默认选中某一行的 Cell。

错误方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// Controller
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"xxxCellId") forIndexPath:indexPath];
if (/* ... */) {
cell.selected = YES;
}
return cell;
}

// View
- (void)setSelected:(BOOL)selected {
[super setSelected:selected];
if (selected) {
// do something
} else {
// do something
}
}

这种做法无效。
测试发现,tableView:cellForRowAtIndexPath 方法执行完后,会自动调用 setSelected: 方法,而此时传入的参数均为 NO,所以导致此前手动调用 setSelected: 方法无效。

Read more »

CADisplayLink 是一个能以和屏幕刷新率相同的频率将内容画到屏幕上的定时器。

A timer object that allows your application to synchronize its drawing to the refresh rate of the display.

创建一个 CADisplayLink 对象,设置好 target 、selector ,并注册到 runloop 中,每次屏幕刷新时就会调用 target 上的 selector。

属性 & 方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
+ (CADisplayLink *)displayLinkWithTarget:(id)target selector:(SEL)sel;

- (void)addToRunLoop:(NSRunLoop *)runloop forMode:(NSRunLoopMode)mode;

- (void)removeFromRunLoop:(NSRunLoop *)runloop forMode:(NSRunLoopMode)mode;

// 时间戳
@property(readonly, nonatomic) CFTimeInterval timestamp;

// 每帧之间的时间间隔(单位:秒)
@property(readonly, nonatomic) CFTimeInterval duration;

// 暂停
@property(getter=isPaused, nonatomic) BOOL paused;

// 可读可写,用来设置多少次屏幕刷新触发一次 selector 。默认是1,即每次都出发。
// 比如iOS设备的屏幕刷新频率为60Hz,也就是每秒60次,当这个值设置为2时,selector 每秒触发30次。
@property(nonatomic) NSInteger frameInterval
CA_AVAILABLE_BUT_DEPRECATED_IOS (3.1, 10.0, 9.0, 10.0, 2.0, 3.0, "use preferredFramesPerSecond");
@property(nonatomic) NSInteger preferredFramesPerSecond CA_AVAILABLE_IOS_STARTING(10.0, 10.0, 3.0);

当不需要使用 DisplayLink 时,应该先调用- (void)invalidate;方法,方法里面会将其从 runloop 中移除。最后将对象释放掉。
另外注意 CADisplayLink 不能被继承。

Read more »

序言

项目中,我用腾讯的 Bugly 作为项目统计和错误分析工具。一般在 Bugly 中分析出错堆栈时,用符号表可以更加精确的定位 bug 出现的地方。

什么是符号表

符号表是内存地址与函数名、文件名、行号的映射表。格式如下:

<起始地址> <结束地址> <函数> [<文件名:行号>]

符号表的作用

应用发生 crash 时的堆栈信息,一般都是二进制的地址信息。直接使用二进制信息很难定位问题,我们需要将这些二进制信息还原成代码所在的具体位置(哪个文件下,哪个方法,行号是多少)。有了这些信息,定位问题发生的位置就简单多了。这时候就需要符号表了。
符号表的作用就是:它能对 crash 的堆栈进行解析和还原,准确定位 APP 发生 crash 的地方。

符号表 UUID

每打一个包,都会生成一个新的 UUID ,也就是说 .app 符号表 dSYM 文件拥有一个相同的 UUID 。当程序出现问题时,就是通过这个 UUID 找到对应的符号表的。

dSYM 文件

dSYM 文件是指具有调试信息的目标文件,通过工具可以生成符号表文件。目前 Bugly 已经支持直接上传 dSYM 文件了。

Read more »

序言

Base64 编码

原理

Base64 编码会把 3 字节的二进制数据编码为 4 字节的数据,长度增加 33% 。如果要编码的二进制数据不是 3 的倍数,Base64 会用 \x00 字节在末尾补齐,然后在末尾加上1、2个 = 号,表示补的字节数。例如:


需要加密的数据:s 1 3
对应的 ascii:115 49 51
2进制: 01110011 00110001 00110011
转换:每三个字节转换成四个字节
转换后: 011100.11 0011.0001 00.110011 (标点处分割)
转换后: 011100 110011 000100 110011
高两位自动补0
最终数据: 00011100 00110011 00000100 00110011
得到 28 51 4 51
查对下照表 c z E z


iOS下有两种比较常见的 Base64 编码方式
第一种:iOS自带的编码方式
这里我写了一个分类,可以直接对字符串编码、解码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#import "NSString+VCHBase64.h"

@implementation NSString (VCHBase64)

- (NSString *)vch_base64Encode {
NSData *data = [self dataUsingEncoding:NSUTF8StringEncoding];
return [data base64EncodedStringWithOptions:NSDataBase64Encoding64CharacterLineLength];
}

- (NSString *)vch_base64Decode {
NSData *data = [[NSData alloc] initWithBase64EncodedString:self options:NSDataBase64DecodingIgnoreUnknownCharacters];
return [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
}
@end

第二种:用 Google 的 GTMBase64
可以直接在GitHub上搜索。下面的 Demo 里面也有。

MD5 加密

加密代码

1
2
3
4
5
6
7
8
9
10
11
12
#import <CommonCrypto/CommonDigest.h>

- (NSString *)vch_md5 {
const char *cStr = [self UTF8String];
unsigned char digest[CC_MD5_DIGEST_LENGTH];
CC_MD5(cStr, (uint32_t)strlen(cStr), digest);
NSMutableString *output = [NSMutableString stringWithCapacity:CC_MD5_DIGEST_LENGTH * 2];
for(int i = 0; i < CC_MD5_DIGEST_LENGTH; i++) {
[output appendFormat:@"%02x", digest[i]];
}
return output;
}

MD5 加密一般是不可解密的,但可以通过穷举法解密(就是一个一个去匹配)。我们可以给 MD5 加个数字,然后再加密一次,那么这样加密后就基本无法再解密出来了。

AES 加密

AES 加密、解密需要同一个密钥,这种加密方法称为单密钥加密,也称对称加密。
AES 有多种加密方式(ECB、CBC、CFB、OFB),如果使用 CBC 方式加密,那么还需要提供密钥偏移量 IV 这个值。
AES 可以采用128位 或者 256位的加密方式。

下面代码采用了 AES256 CBC 模式。

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
#import "NSString+VCHAES.h"
#import <CommonCrypto/CommonCryptor.h>

@implementation NSString (VCHAES)

- (NSString *)vch_AESEncryptWithKey:(NSString *)key iv:(NSString *)iv {
NSData *data = [self dataUsingEncoding:NSUTF8StringEncoding];
NSData *encryptData = [self AES256operation:kCCEncrypt data:data key:key iv:iv];
NSString *encryptString = [encryptData base64EncodedStringWithOptions:NSDataBase64Encoding64CharacterLineLength];
return encryptString;
}

- (NSString *)vch_AESDecryptWithKey:(NSString *)key iv:(NSString *)iv {
NSData *data = [[NSData alloc] initWithBase64EncodedString:self options:NSDataBase64DecodingIgnoreUnknownCharacters];
NSData *decryptData = [self AES256operation:kCCDecrypt data:data key:key iv:iv];
NSString *decryptString = [[NSString alloc] initWithData:decryptData encoding:NSUTF8StringEncoding];
return decryptString;
}

- (NSData *)AES256operation:(CCOperation)operation data:(NSData *)data key:(NSString *)key iv:(NSString *)iv {
char keyPtr[kCCKeySizeAES256 + 1];
bzero(keyPtr, sizeof(keyPtr));
[key getCString:keyPtr maxLength:sizeof(keyPtr) encoding:NSUTF8StringEncoding];

char ivPtr[kCCKeySizeAES256 + 1];
bzero(ivPtr, sizeof(ivPtr));
[iv getCString:ivPtr maxLength:sizeof(ivPtr) encoding:NSUTF8StringEncoding];
size_t bufferSize = [data length] + kCCKeySizeAES256;
void *buffer = malloc(bufferSize);
size_t numBytesEncrypted = 0;

CCCryptorStatus cryptorStatus = CCCrypt(operation, kCCAlgorithmAES, kCCOptionPKCS7Padding,
keyPtr, kCCKeySizeAES256, ivPtr, [data bytes], [data length],
buffer, bufferSize, &numBytesEncrypted);
if(cryptorStatus == kCCSuccess) {
return [NSData dataWithBytesNoCopy:buffer length:numBytesEncrypted];
}
free(buffer);
return nil;
}
@end

这里的 key 和 iv ,是由加密者提供的。

Demo

Github Demo