软件开发实践学习记录

  • Home

  • Tags

  • Categories

  • Archives

FBAllocationTracker 源码阅读

Posted on 2019-08-01 | Edited on 2019-08-05

FBAllocationTracker有两种模式,跟踪对象,基数allocs/deallocs。

主要类分析

FBAllocationTrackerManager

本类是提供给使用者的最外层包装。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@interface FBAllocationTrackerManager : NSObject

+ (nullable instancetype)sharedManager;
//启动track
- (void)startTrackingAllocations;
- (void)stopTrackingAllocations;
- (BOOL)isAllocationTrackerEnabled;

- (nullable NSArray<FBAllocationTrackerSummary *> *)currentAllocationSummary;
//使用tracking对象模式
- (void)enableGenerations;
- (void)disableGenerations;
//类似Instruments中allocation提供的mark功能
- (void)markGeneration;

- (nullable NSArray<NSArray<FBAllocationTrackerSummary *> *> *)currentSummaryForGenerations;
- (nullable NSArray *)instancesForClass:(nonnull __unsafe_unretained Class)aCls
inGeneration:(NSInteger)generation;
- (nullable NSArray *)instancesOfClasses:(nonnull NSArray *)classes;
- (nullable NSSet<Class> *)trackedClasses;
@end

实现中,多是通过FBAllocationTrackerImpl来完成。

FBAllocationTrackerImpl

1
2
3
4
5
6
7
8
9
10
11
12
AllocationSummary allocationTrackerSummary();
//启动&关闭track
void beginTracking();
void endTracking();
bool isTracking();
//启动&关闭generation
void enableGenerations();
void disableGenerations();
//mark
void markGeneration();

FullGenerationSummary generationSummary();

这个类本身实现了allocs和deallocs计数,通过Generation类实现track对象功能,后者下面会说到。

与其他内存监控相关库类似,首先需要 hook alloc 和 dealloc。这部分逻辑大同小异。

准备 - 将 allocWithZone与 dealloc 的函数实现,复制到准备好的空方法上

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
void prepareOriginalMethods(void) {
if (_didCopyOriginalMethods) {
return;
}
// prepareOriginalMethods called from turnOn/Off which is synced by
// _lock, this is thread-safe
_didCopyOriginalMethods = true;

//copy方法的实现到fb_originalAllocWithZone和fb_originalDealloc
//使用_didCopyOriginalMethods保证只copy一次
replaceSelectorWithSelector([NSObject class],
@selector(fb_originalAllocWithZone:),
@selector(allocWithZone:),
FBClassMethod);

replaceSelectorWithSelector([NSObject class],
@selector(fb_originalDealloc),
sel_registerName("dealloc"),
FBInstanceMethod);
}

开始 - 用自己的alloc和dealloc替换系统的

1
2
3
4
5
6
7
8
9
10
11
void turnOnTracking(void) {
prepareOriginalMethods();
replaceSelectorWithSelector([NSObject class],
@selector(allocWithZone:),
@selector(fb_newAllocWithZone:),
FBClassMethod);
replaceSelectorWithSelector([NSObject class],
sel_registerName("dealloc"),
@selector(fb_newDealloc),
FBInstanceMethod);
}

关闭 - 用一开始保存的系统方法,再替换回去

1
2
3
4
5
6
7
8
9
10
11
12
13
void turnOffTracking(void) {
prepareOriginalMethods();

replaceSelectorWithSelector([NSObject class],
@selector(allocWithZone:),
@selector(fb_originalAllocWithZone:),
FBClassMethod);

replaceSelectorWithSelector([NSObject class],
sel_registerName("dealloc"),
@selector(fb_originalDealloc),
FBInstanceMethod);
}

自定义的alloc和dealloc方法除了调用系统实现外,额外调用了记录该类的方法。

1
2
3
4
5
6
7
8
9
10
11
12
+ (id)fb_newAllocWithZone:(id)zone
{
id object = [self fb_originalAllocWithZone:zone];
FB::AllocationTracker::incrementAllocations(object);
return object;
}

- (void)fb_newDealloc
{
FB::AllocationTracker::incrementDeallocations(self);
[self fb_originalDealloc];
}

incrementAllocations做两件事,将该对象的allocs记数+1。如果启用了generation,就将对象记录到generation中。incrementDeallocations是相反的步骤。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
void incrementAllocations(__unsafe_unretained id obj) {
Class aCls = [obj class];

if (!_shouldTrackClass(aCls)) {
return;
}

std::lock_guard<std::mutex> l(*_lock);

if (_trackingInProgress) {
(*_allocations)[aCls]++;
}

if (_generationManager) {
_generationManager->addObject(obj);
}
}

Generation

generation主要是用来记录对象。因为可以分代记录。所以generation中有两个集合对象。每一代的对象,记录在同一个GenerationMap中。所有的GenerationMap都存在GenerationList中。markGeneration会创建一个新的GenerationMap。

总结

FBAllocationTracker通过对alloc和dealloc的hook。记录创建和销毁的对象。这部分和其他内存相关的监控类似。此外借鉴了 instruments 工具中 allocation 部分的 markgeneration,提供了分代记录的功能。实现也比较容易,通过集合来存储generation。每次记录对象,就从集合中取最后一个generation来添加即可。

MMKV

Posted on 2019-07-26 | Edited on 2019-07-30

介绍

MMKV是基于mmap的键值存储库。提供了类似NSUserDefaults的功能。

MMKV的基础 - MMAP

mmap主要有2种用法,一个是建立匿名映射,可以起到父子进程之间共享内存的作用。另一个是磁盘文件映射进程的虚拟地址空间。MMKV就是用的磁盘文件映射。

mmap的主要的好处在于,减少一次内存拷贝。在我们平时的read/write系统调用中,文件内容的拷贝要多经历内核缓冲区这个阶段,所以比mmap多了一次内存拷贝,mmap只有用户空间的内存拷贝(这个阶段read/write也有)。正是因为减少了从Linux的页缓存到用户空间的缓冲区的这一次拷贝,所以mmap大大提高了性能,mmap也被称为zero-copy技术。

使用步骤

  • 创建文件或者指定文件
  • 打开文件
  • 调整文件大小(非必须步骤)
  • mmap内存映射
  • 拷贝内容到映射区
  • 扩容 (看需要)
  • munmap结束映射
  • 关闭文件

创建或者打开文件

没什么可说的,指定路径,创建文件。

打开文件

使用open函数,返回文件句柄

1
2
3
4
5
_fd = open([url UTF8String], O_RDWR,S_IRWXU);
if (_fd < 0) {
NSLog(@"fail to open file:%@",url);
return;
}

获取文件大小

1
2
3
4
5
size_t fileSize = 0;
struct stat st = {};
if (fstat(_fd, &st) != -1) {
fileSize = (size_t) st.st_size;
}

调整文件大小

如果设置的比文件小,则会截取文件。

1
2
3
4
5
6
7
8
9
10
11
//代表将文件中多大的部分对应到内存。以字节为单位,不足一内存页按一内存页处理
//向上取整,找到pagesize的整倍数
size_t pageSize = getpagesize();
if (fileSize == 0 || fileSize/pageSize != 0) {
_mmapSize = (fileSize/pageSize + 1) * pageSize;
if (ftruncate(_fd, _mmapSize) != 0) {
return;
}
}else {
_mmapSize = pageSize;
}

文件内存映射

1
2
3
4
5
6
7
8
9
10
11
12
void *start = NULL; //由系统选定地址
off_t offset = 0;//offset为文件映射的偏移量,通常设置为0,代表从文件最前方开始对应,offset必须是分页大小的整数倍。可以简单理解为被映射对象内容的起点。
_ptr = (char *) mmap(start, _mmapSize, PROT_READ | PROT_WRITE, MAP_SHARED, _fd, offset);
if (_ptr == MAP_FAILED) {
NSLog(@"mmap失败,%s",strerror(errno));
//EBADF 参数fd 不是有效的文件描述词
//EACCES 存取权限有误。如果是MAP_PRIVATE 情况下文件必须可读,使用MAP_SHARED则要有PROT_WRITE以及该文件要能写入。
//EINVAL 参数start、length 或offset有一个不合法。
//EAGAIN 文件被锁住,或是有太多内存被锁住。
//ENOMEM 内存不足。
return;
}

函数原型为void *mmap(void *start,size_t length,int prot,int flags,int fd,off_t offsize);
参数介绍:

  • start 传入一个期望的映射起始地址。同常传入null,由系统寻找合适的内存区域,并将地址返回。

  • length 传入映射的长度

  • port 映射区域的操作属性,有如下四种类型,这里我们使用读写属性。

    1
    2
    3
    4
    #define	PROT_NONE	0x00	/* [MC2] no permissions */
    #define PROT_READ 0x01 /* [MC2] pages can be read */
    #define PROT_WRITE 0x02 /* [MC2] pages can be written */
    #define PROT_EXEC 0x04 /* [MC2] pages can be executed */
  • flag 会影响映射区域的各种特性,可以看下定义,类型比较多

  • fd 打开的文件句柄

  • offset 为文件映射的偏移量,通常设置为0,代表从文件最前方开始对应

扩容

需要三个步骤,使用ftruncate扩容文件,munmap结束映射,使用新的大小,重新映射。比如如下方法,是一个添加数据的方法,内存不够会扩容后继续添加

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
- (void)appendData: (NSData *)data {
if ((_offset + data.length) > _mmapSize) {
off_t newSize = _mmapSize + getpagesize();
if (ftruncate(_fd, newSize) != 0) {
NSLog(@"fail to truncate [%zu] to size %lld, %s", _mmapSize, newSize, strerror(errno));
return;
}
if (munmap(_ptr, _mmapSize) != 0) {
NSLog(@"fail to munmap, %s", strerror(errno));
return;
}
_mmapSize = newSize;
_ptr = (char *) mmap(NULL, _mmapSize, PROT_READ | PROT_WRITE, MAP_SHARED, _fd, 0);
if (_ptr == MAP_FAILED) {
NSLog(@"mmap失败,%s",strerror(errno));
return;
}
}
memcpy(_ptr + _offset, data.bytes, data.length);
_offset = _offset + data.length;
}

MMKV 数据处理概要

一个MMKV实例会产生两个文件,内容文件,CRC校验文件。校验文件就是存储了内容文件中内容部分的CRC校验值。内容文件的前四个字节存储内容长度,后面这个长度的数据是实际内容。因为文件扩容是以页的整数倍。所以可能还会有一些空白内容在最后。

MMKV启动会读取文件,根据文件长度,把实际数据读取出来转成字典。其中还会进行CRC校验。

后续的操作,不管是读还是写,都是对这个字典进行操作。写操作之后会设置m_hasFullWriteBack = NO;。表示有内容没有写回。之后会在合适的时机调用[self fullWriteBack]进行数据写入。

MMKV set的实现

set的基本逻辑

字典中存的值都是NSData类型,所以数据在存入字典前,需要进行一下转化,如下是基本逻辑。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
- (BOOL)setFloat:(float)value forKey:(NSString *)key {
if (key.length <= 0) {
return NO;
}
//获取Float类型的长度,4个字节
size_t size = pbFloatSize(value);
//创建一个data,用来保存这个float
NSMutableData *data = [NSMutableData dataWithLength:size];
//构建MiniCodedOutputData,来做float转data
MiniCodedOutputData output(data);
//将float写入data
output.writeFloat(value);
//将数据写入词典
return [self setRawData:data forKey:key];
}

MMKV 实现了MiniCodedOutputData类,来处理字节信息。下面的类型到Data的转换就是通过此类完成。首先writeRawByte方法,提供安字节写入的功能,直接填充一个字节,然后位置后移一位

1
2
3
4
5
6
7
void MiniCodedOutputData::writeRawByte(uint8_t value) {
if (m_position == m_size) {
NSString *reason = [NSString stringWithFormat:@"position: %d, bufferLength: %u", m_position, (unsigned int) m_size];
@throw [NSException exceptionWithName:@"OutOfSpace" reason:reason userInfo:nil];
}
m_ptr[m_position++] = value;
}

不同的数据转成NSData的逻辑不一样,整体逻辑是分割成字节,然后使用writeRawByte写入

Bool类型

Bool类型只占一个字节,直接写入即可

1
2
3
void MiniCodedOutputData::writeBool(BOOL value) {
this->writeRawByte(value ? 1 : 0);
}

Float类型

32位float类型按照小顶端顺序分成4个字节存入

1
2
3
4
5
6
void MiniCodedOutputData::writeRawLittleEndian32(int32_t value) {
this->writeRawByte((value) &0xff);
this->writeRawByte((value >> 8) & 0xff);
this->writeRawByte((value >> 16) & 0xff);
this->writeRawByte((value >> 24) & 0xff);
}

64位的类似

1
2
3
4
5
6
7
8
9
10
void MiniCodedOutputData::writeRawLittleEndian64(int64_t value) {
this->writeRawByte((int32_t)(value) &0xff);
this->writeRawByte((int32_t)(value >> 8) & 0xff);
this->writeRawByte((int32_t)(value >> 16) & 0xff);
this->writeRawByte((int32_t)(value >> 24) & 0xff);
this->writeRawByte((int32_t)(value >> 32) & 0xff);
this->writeRawByte((int32_t)(value >> 40) & 0xff);
this->writeRawByte((int32_t)(value >> 48) & 0xff);
this->writeRawByte((int32_t)(value >> 56) & 0xff);
}

Int类型

Int类型的存储逻辑稍有不同

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
void MiniCodedOutputData::writeInt64(int64_t value) {
this->writeRawVarint64(value);
}

void MiniCodedOutputData::writeRawVarint64(int64_t value) {
while (YES) {
//对0x7FL的取反,在& 结果为0,意味着只有后七位有值
if ((value & ~0x7FL) == 0) {
this->writeRawByte((int32_t) value);
return;
} else {
//(value & 0x7f) | 0x80
// 先 & 0111 1111 :取后七位,在 | 10000000 : 首位填1
this->writeRawByte(((int32_t) value & 0x7f) | 0x80);
//写入后,右移7位继续
value = logicalRightShift64(value, 7);
}
}
}

64位Int类型是每次存7位,首位填1的方式存储。共需要10个字节。7*9 = 63,最后一个字节只存一位。这里每次只处理7位的原因,会在后续进行说明。

32位的Int类型类似

1
2
3
4
5
6
7
8
9
10
11
void MiniCodedOutputData::writeRawVarint32(int32_t value) {
while (YES) {
if ((value & ~0x7f) == 0) {
this->writeRawByte(value);
return;
} else {
this->writeRawByte((value & 0x7f) | 0x80);
value = logicalRightShift32(value, 7);
}
}
}

NSData类型

data类型的写入,需要记录data的长度,以及值

1
2
3
4
5
6
void MiniCodedOutputData::writeData(NSData *value) {
//写入data的长度
this->writeRawVarint32((int32_t) value.length);
//写入data本身
this->writeRawData(value);
}

长度的写入已经介绍过了,下面看下Data类型的写入。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
void MiniCodedOutputData::writeRawData(NSData *data) {
this->writeRawData(data, 0, (int32_t) data.length);
}

void MiniCodedOutputData::writeRawData(NSData *value, int32_t offset, int32_t length) {
if (length <= 0) {
return;
}
if (m_size - m_position >= length) {
memcpy(m_ptr + m_position, ((uint8_t *) value.bytes) + offset, length);
m_position += length;
} else {
[NSException exceptionWithName:@"Space" reason:@"too much data than calc" userInfo:nil];
}
}

data类型比较容易处理,直接内存拷贝即可。

NSString类型

1
2
3
4
5
6
7
8
9
10
11
12
void MiniCodedOutputData::writeString(NSString *value) {
NSUInteger numberOfBytes = [value lengthOfBytesUsingEncoding:NSUTF8StringEncoding];
this->writeRawVarint32((int32_t) numberOfBytes);
[value getBytes:m_ptr + m_position
maxLength:numberOfBytes
usedLength:0
encoding:NSUTF8StringEncoding
options:0
range:NSMakeRange(0, value.length)
remainingRange:nullptr];
m_position += numberOfBytes;
}

NSString 类型和Data类似,先写入长度,在转成Data存入。NSString有个方便的方法,直接将数据放到指定的内存位置。这里正好合用。

取数据的基本逻辑

取数据的逻辑是对set的反向操作,先从字典中取出数据,然后转成具体类型。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
- (int64_t)getInt64ForKey:(NSString *)key {
return [self getInt64ForKey:key defaultValue:0];
}
- (int64_t)getInt64ForKey:(NSString *)key defaultValue:(int64_t)defaultValue {
if (key.length <= 0) {
return defaultValue;
}
//从通过key从字典中取出数据,类型是NSData
NSData *data = [self getRawDataForKey:key];
if (data.length > 0) {
@try {
//将Data类型转成Int64
MiniCodedInputData input(data);
return input.readInt64();
} @catch (NSException *exception) {
MMKVError(@"%@", exception);
}
}
return defaultValue;
}

MMKV实现了MiniCodedInputData来将NSData转成需要的类型。这个类的操作是MiniCodedOutputData的反向操作。其他都比较类似,这里看一下readRawVarint64方法,解释下之前的留下的小问题,为什么要每7位存一个字节。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
int64_t MiniCodedInputData::readRawVarint64() {
int32_t shift = 0;
int64_t result = 0;
while (shift < 64) {
int8_t b = this->readRawByte();
result |= (int64_t)(b & 0x7f) << shift;
if ((b & 0x80) == 0) {
return result;
}
shift += 7;
}
@throw [NSException exceptionWithName:@"InvalidProtocolBuffer" reason:@"malformedVarint" userInfo:nil];
return -1;
}

读取的字节是存到一个8位Int中 int8_t b = this->readRawByte();。前面的逻辑中每次存入七位,首位填1,因为首位是填充的,没有意义,这里通过 & 0x7f直接移除。如果是8位存储,首位的1会使这个数据变成负数,这里的处理就会变得比较麻烦。

PLeakSniffer 源码阅读

Posted on 2019-07-18 | Edited on 2019-08-01

介绍

关于PLeakSniffer的介绍可以直接看作者的文章。下面是原文中的介绍。

子对象(比如view)建立一个对controller的weak引用,如果Controller被释放,这个weak引用也随之置为nil。那怎么知道子对象没有被释放呢?用一个单例对象每个一小段时间发出一个ping通知去ping这个子对象,如果子对象还活着就回一个pong通知。所以结论就是:如果子对象的controller已不存在,但还能响应这个ping通知,那么这个对象就是可疑的泄漏对象。

使用

介绍比较简单,作者给出的用法如下。

1
2
3
4
#if MY_DEBUG_ENV
[[PLeakSniffer sharedInstance] installLeakSniffer];
[[PLeakSniffer sharedInstance] addIgnoreList:@[@"MySingletonController"]];
#endif

实现

PLeakSnifferCitizen

1
2
3
4
5
@protocol PLeakSnifferCitizen <NSObject>
+ (void)prepareForSniffer;
- (BOOL)markAlive;
- (BOOL)isAlive;
@end

如果要对某个类型进行内存检查,这个对象要实现这个协议。下面看看库支持的三种类型的实现。

NSObject

NSObject的分类实现了markAlive方法。另外两个方法并没有实现。所以并不是所有继承自NSObject的类型都会被追踪。主要是这个方法逻辑覆盖了三种库里支持的类型,所以在这里写,省得每个类型都写一遍。下面看看这个方法的具体逻辑

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
- (BOOL)markAlive
{
if ([self pProxy] != nil) {
return false;
}

//不处理系统类型
NSString* className = NSStringFromClass([self class]);
if ([className hasPrefix:@"_"] || [className hasPrefix:@"UI"] || [className hasPrefix:@"NS"]) {
return false;
}

//如果view的superView是nil,则直接返回false
if ([self isKindOfClass:[UIView class]]) {
UIView* v = (UIView*)self;
if (v.superview == nil) {
return false;
}
}

//controller需要在navigation栈中,或者是被present出来,否则返回false
if ([self isKindOfClass:[UIViewController class]]) {
UIViewController* c = (UIViewController*)self;
if (c.navigationController == nil && c.presentingViewController == nil) {
return false;
}
}

//skip some weird system classes
//跳过一些诡异的系统类(这个好像和上边的UI打头的类型判断重复了)
static NSMutableDictionary* ignoreList = nil;
@synchronized (self) {
if (ignoreList == nil) {
ignoreList = @{}.mutableCopy;
NSArray* arr = @[@"UITextFieldLabel", @"UIFieldEditor", @"UITextSelectionView",
@"UITableViewCellSelectedBackground", @"UIView", @"UIAlertController"];
for (NSString* str in arr) {
ignoreList[str] = @":)";
}
}
if ([ignoreList objectForKey:NSStringFromClass([self class])]) {
return false;
}
}

PObjectProxy* proxy = [PObjectProxy new];
//给当前对象设置一个代理对象,用于后续的ping操作
[self setPProxy:proxy];
[proxy prepareProxy:self];

return true;
}

这个方法有点长,因为要分类型处理。主要做两个事情(有点违背方法的单一职责)。

  • 如果不符合一些对象的alive定义,则直接返回false,告诉调用方,对象是非alive状态
  • 如果对象是alive状态,添加监测代理,用于后续ping对象。

UIViewController

UIViewController的分类实现了prepareForSniffer 和isAlive。markAlive通过继承NSObject获得。

1
2
3
4
5
6
7
8
+ (void)prepareForSniffer
{
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
[self swizzleSEL:@selector(presentViewController:animated:completion:) withSEL:@selector(swizzled_presentViewController:animated:completion:)];
[self swizzleSEL:@selector(viewDidAppear:) withSEL:@selector(swizzled_viewDidAppear:)];
});
}

prepareForSniffer做了两个hook。

  • hook controller的present方法,对present出来的controller,调用markAlive。
  • Hook controller的viewDidAppear方法,对属性进行track。

第二步,属性的track,其实就是递归调用属性的markAlive,添加proxy。

isAlive的判断逻辑

  • 所属view在UIWindow视图层级中
  • 本身在navigation栈中,或者是由其他controller present出来。

主要代码

1
2
3
4
5
6
7
8
9
10
11
UIView* v = self.view;
while (v.superview != nil) {
v = v.superview;
}
if ([v isKindOfClass:[UIWindow class]]) {
visibleOnScreen = true;
}
BOOL beingHeld = false;
if (self.navigationController != nil || self.presentingViewController != nil) {
beingHeld = true;
}

UINavigationController

UINavigationController只实现了prepareForSniffer,isAlive继承自UIViewController。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
+ (void)prepareForSniffer
{
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
[self swizzleSEL:@selector(pushViewController:animated:) withSEL:@selector(swizzled_pushViewController:animated:)];
});
}

- (void)swizzled_pushViewController:(UIViewController *)viewController animated:(BOOL)animated {

[self swizzled_pushViewController:viewController animated:animated];

[viewController markAlive];

}

Hook push方法,对push的controller,调用markAlive。

UIView

UIView继承自NSObject,还需要实现prepareForSniffer和isAlive。跟前面几个的实现类似。hook一个合适的时机,调用markAlive。

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
+ (void)prepareForSniffer
{
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
[self swizzleSEL:@selector(didMoveToSuperview) withSEL:@selector(swizzled_didMoveToSuperview)];
});
}

- (void)swizzled_didMoveToSuperview
{
[self swizzled_didMoveToSuperview];

BOOL hasAliveParent = false;

UIResponder* r = self.nextResponder;
while (r) {
if ([r pProxy] != nil) {
hasAliveParent = true;
break;
}
r = r.nextResponder;
}

if (hasAliveParent) {
[self markAlive];
}
}

isAlive的判断跟之前controller中isAlive的判断前半部分是一样的,通过查看view的最顶层view是不是UIWindow来判断alive。

PObjectProxy

proxy主要做两件事

  • 注册通知接收ping的触发
  • 检查宿主是否在不应存活时,还活着,通知出去,此处可能有内存泄漏

注册通知

1
2
3
4
5
- (void)prepareProxy:(NSObject*)target {
self.weakTarget = target;
[[NSNotificationCenter defaultCenter] removeObserver:self name:Notif_PLeakSniffer_Ping object:nil];
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(detectSnifferPing) name:Notif_PLeakSniffer_Ping object:nil];
}

检查是否泄漏,可能泄漏就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
- (void)detectSnifferPing
{
if (self.weakTarget == nil) {
return;
}
if (_hasNotified) {
return;
}
//如果proxy的target不应该alive。就记录一次fail
BOOL alive = [self.weakTarget isAlive];
if (alive == false) {
_leakCheckFailCount ++;
}
//fail次数到达5次以上就会进行提醒,5次大概是2.5秒
if (_leakCheckFailCount >= kPObjectProxyLeakCheckMaxFailCount) {
[self notifyPossibleMemoryLeak];
}
}

- (void)notifyPossibleMemoryLeak
{
if (_hasNotified) {
return;
}
_hasNotified = true;
dispatch_async(dispatch_get_main_queue(), ^{
[[NSNotificationCenter defaultCenter] postNotificationName:Notif_PLeakSniffer_Pong object:self.weakTarget];
});
}

流程

有了上边的基础设施,下面的流程就比较简单了。流程逻辑主要在PLeakSniffer中。使用了一个timer,两个通知。从使用介绍来看,installLeakSniffer是起点。

1
2
3
4
5
6
- (void)installLeakSniffer {
[UINavigationController prepareForSniffer];
[UIViewController prepareForSniffer];
[UIView prepareForSniffer];
[self startPingTimer];
}

三个类型的prepare,以及启动ping定时。三个prepare逻辑上边分析过了,navigationController和UIView只是在合适的时间markAlive。UIViewControler除了在present时对presentingController进行标记,还需要对自己的属性进行递归标记。prepare流程结束后,所有之后的controller和View都会纳入监控(通过设置proxy)。

startPingTimer会检查是否在主线程,如果不在主线程,dispatch到主线程。然后创建timer,每0.5秒调用一次sendPing。sendPing就是post一个通知。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
- (void)startPingTimer
{
//检查是否在主线程,非主线程,则dispatch到主线程
if ([NSThread isMainThread] == false) {
dispatch_async(dispatch_get_main_queue(), ^{
[self startPingTimer];
return ;
});
}

if (self.pingTimer) {
return;
}
//开启定时,每0.5秒,调用一次sendPing
self.pingTimer = [NSTimer scheduledTimerWithTimeInterval:kPLeakSnifferPingInterval target:self selector:@selector(sendPing) userInfo:nil repeats:true];
}

- (void)sendPing
{
//发通知
[[NSNotificationCenter defaultCenter] postNotificationName:Notif_PLeakSniffer_Ping object:nil];
}

前面看过了proxy逻辑,这里sendPing的通知会到proxy中,proxy会检查是否有可能泄漏。如果有泄漏,会通过通知发出去。而接受的地方还是PLeakSniffer。接收之后的处理比较简单。如果是需要被忽略的就丢弃。否则alert出来,活着print。

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
- (void)detectPong:(NSNotification*)notif
{
NSObject* leakedObject = notif.object;
NSString* leakedName = NSStringFromClass([leakedObject class]);
@synchronized (self) {
if ([_ignoreList containsObject:leakedName]) {
return;
}
}

//we got a leak here
if (_useAlert) {
NSString* msg = [NSString stringWithFormat:@"Detect Possible Leak: %@", [leakedObject class]];
UIAlertView *alertView = [[UIAlertView alloc] initWithTitle:@"PLeakSniffer" message:msg delegate:nil cancelButtonTitle:nil otherButtonTitles:@"OK", nil];
[alertView show];
}
else
{
if ([leakedObject isKindOfClass:[UIViewController class]]) {
PLeakLog(@"\n\nDetect Possible Controller Leak: %@ \n\n", [leakedObject class]);
}
else
{
PLeakLog(@"\n\nDetect Possible Leak: %@ \n\n", [leakedObject class]);
}
}
}

补充

前面提到UIViewController的prepareSnifferhook了viewDidAppear方法,对属性进行track。这个操作会减少遗漏,最大程度上找到所有可能出现内存问题的地方。但是属性量比较大,不知道这里会不会有内存问题。平常runtime用的少,这里看看别人的实践。

获取所有属性

1
objc_property_t* properties = class_copyPropertyList(cls, &count );

判断属性是否是强引用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
bool isStrongProperty(objc_property_t property)
{
const char* attrs = property_getAttributes( property );
if (attrs == NULL)
return false;

const char* p = attrs;
//property中有'&'则为强引用
p = strchr(p, '&');
if (p == NULL) {
return false;
}
else
{
return true;
}
}

kingfisher源码阅读

Posted on 2019-07-01 | Edited on 2019-07-30

模块介绍

通用模块

ImageSource

Kingfisher中对图片资源的表示有两种,定义在Source枚举中。

1
2
3
4
public enum Source {
case network(Resource)
case provider(ImageDataProvider)
}

network类型的需要提供一个url和cacheKey,然后kingfisher会自行下载。provider类型的,是自己提供image数据。下面看下Resource

1
2
3
4
5
6
7
8
public protocol Resource {

/// The key used in cache.
var cacheKey: String { get }

/// The target image URL.
var downloadURL: URL { get }
}

Resource的协议需要两个属性,下载地址和缓存key。正常情况下我们无需实现这个Resource,因为Kingfisher中的URL通过extension实现了这个协议,我们平常直接用URL就可以了。

1
2
3
4
extension URL: Resource {
public var cacheKey: String { return absoluteString }
public var downloadURL: URL { return self }
}

但是,如果你想定制key的格式,可以使用Resource的另一个实现。

1
2
3
4
5
6
7
8
public struct ImageResource: Resource {
public init(downloadURL: URL, cacheKey: String? = nil) {
self.downloadURL = downloadURL
self.cacheKey = cacheKey ?? downloadURL.absoluteString
}
public let cacheKey: String
public let downloadURL: URL
}

ImageDataProvider 提供了三个实现,分别是LocalFileImageDataProvider,Base64ImageDataProvider,RawImageDataProvider。三种方式提供image的data,不过这个没什么使用场景,源码中,也只在测试的地方有用到。

KingfisherError

1
2
3
4
5
6
7
8
9
10
11
12
public enum KingfisherError: Error {
/// Represents the error reason during networking request phase.
case requestError(reason: RequestErrorReason)
/// Represents the error reason during networking response phase.
case responseError(reason: ResponseErrorReason)
/// Represents the error reason during Kingfisher caching system.
case cacheError(reason: CacheErrorReason)
/// Represents the error reason during image processing phase.
case processorError(reason: ProcessorErrorReason)
/// Represents the error reason during image setting in a view related class.
case imageSettingError(reason: ImageSettingErrorReason)
}

Kingfisher 将 error 按照过程划分,分别是请求,返回,缓存,处理,设置,每个过程一种 error 类型。

###下载

当缓存没有命中的时候,就会启动一个下载。下载主要用到两个类,ImageDownloader启动下载,并处理回调,SessionDelegate处理URLSessionDataTask的下载代理,并提供任务管理功能。图片下载过程中,会通过ImageDownloaderDelegate通知外部。

此外可以通过ImageDownloadRequestModifier拦截修改图片的url。

图片处理

Kingfisher提供丰富的图片处理功能。这些功能通过ImageProcessor来提供,如果有需要其他的处理,可以自行实现这个协议来提供,比如webp格式的图片处理。

Kingfisher在demo中列出了目前已经提供了的。

1
2
3
4
5
6
7
8
9
10
11
12
(DefaultImageProcessor.default, "Default"),
(RoundCornerImageProcessor(cornerRadius: 20), "Round Corner"),
(RoundCornerImageProcessor(cornerRadius: 20, roundingCorners: [.topLeft, .bottomRight]), "Round Corner Partial"),
(BlendImageProcessor(blendMode: .lighten, alpha: 1.0, backgroundColor: .red), "Blend"),
(BlurImageProcessor(blurRadius: 5), "Blur"),
(OverlayImageProcessor(overlay: .red, fraction: 0.5), "Overlay"),
(TintImageProcessor(tint: UIColor.red.withAlphaComponent(0.5)), "Tint"),
(ColorControlsProcessor(brightness: 0.0, contrast: 1.1, saturation: 1.1, inputEV: 1.0), "Vibrancy"),
(BlackWhiteProcessor(), "B&W"),
(CroppingImageProcessor(size: CGSize(width: 100, height: 100)), "Cropping"),
(DownsamplingImageProcessor(size: CGSize(width: 25, height: 25)), "Downsampling"),
(BlurImageProcessor(blurRadius: 5) >> RoundCornerImageProcessor(cornerRadius: 20), "Blur + Round Corner")

这些功能主要通过 ImageDrawing中的方法来实现。

####圆角

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
public func image(withRoundRadius radius: CGFloat,
fit size: CGSize,
roundingCorners corners: RectCorner = .all,
backgroundColor: Color? = nil) -> Image
{
let rect = CGRect(origin: CGPoint(x: 0, y: 0), size: size)
return draw(to: size) { _ in
guard let context = UIGraphicsGetCurrentContext() else {
assertionFailure("[Kingfisher] Failed to create CG context for image.")
return
}

if let backgroundColor = backgroundColor {
let rectPath = UIBezierPath(rect: rect)
backgroundColor.setFill()
rectPath.fill()
}

let path = UIBezierPath(
roundedRect: rect,
byRoundingCorners: corners.uiRectCorner,
cornerRadii: CGSize(width: radius, height: radius)
)
context.addPath(path.cgPath)
context.clip()
base.draw(in: rect)
}
}

draw方法负责开启上下文和关闭上下文,block中是绘制的主要代码。

模糊

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
public func blurred(withRadius radius: CGFloat) -> Image {
// http://www.w3.org/TR/SVG/filters.html#feGaussianBlurElement
// let d = floor(s * 3*sqrt(2*pi)/4 + 0.5)
// if d is odd, use three box-blurs of size 'd', centered on the output pixel.
let s = Float(max(radius, 2.0))
// We will do blur on a resized image (*0.5), so the blur radius could be half as well.

// Fix the slow compiling time for Swift 3.
// See https://github.com/onevcat/Kingfisher/issues/611
let pi2 = 2 * Float.pi
let sqrtPi2 = sqrt(pi2)
var targetRadius = floor(s * 3.0 * sqrtPi2 / 4.0 + 0.5)

if targetRadius.isEven { targetRadius += 1 }

// Determine necessary iteration count by blur radius.
let iterations: Int
if radius < 0.5 {
iterations = 1
} else if radius < 1.5 {
iterations = 2
} else {
iterations = 3
}

let w = Int(size.width)
let h = Int(size.height)
let rowBytes = Int(CGFloat(cgImage.bytesPerRow))

func createEffectBuffer(_ context: CGContext) -> vImage_Buffer {
let data = context.data
let width = vImagePixelCount(context.width)
let height = vImagePixelCount(context.height)
let rowBytes = context.bytesPerRow

return vImage_Buffer(data: data, height: height, width: width, rowBytes: rowBytes)
}

guard let context = beginContext(size: size, scale: scale, inverting: true) else {
assertionFailure("[Kingfisher] Failed to create CG context for blurring image.")
return base
}
context.draw(cgImage, in: CGRect(x: 0, y: 0, width: w, height: h))
endContext()

var inBuffer = createEffectBuffer(context)

guard let outContext = beginContext(size: size, scale: scale, inverting: true) else {
assertionFailure("[Kingfisher] Failed to create CG context for blurring image.")
return base
}
defer { endContext() }
var outBuffer = createEffectBuffer(outContext)

for _ in 0 ..< iterations {
let flag = vImage_Flags(kvImageEdgeExtend)
vImageBoxConvolve_ARGB8888(
&inBuffer, &outBuffer, nil, 0, 0, UInt32(targetRadius), UInt32(targetRadius), nil, flag)
// Next inBuffer should be the outButter of current iteration
(inBuffer, outBuffer) = (outBuffer, inBuffer)
}

let result = outContext.makeImage().flatMap {
Image(cgImage: $0, scale: base.scale, orientation: base.imageOrientation)
}

return blurredImage
}

模糊是通过Accelerate.framework中的vImage实现,使用vImage来做比较自由,但是比较麻烦,需要了解算法细节。

其他的诸如Size,blend都比较简单。

缓存

缓存策略

Kingfisher有两个关于缓存策略的枚举,StorageExpiration用来表示文件创建成功后的缓存策略。

1
2
3
4
5
6
7
8
9
10
11
12
public enum StorageExpiration {
/// The item never expires.
case never
/// The item expires after a time duration of given seconds from now.
case seconds(TimeInterval)
/// The item expires after a time duration of given days from now.
case days(Int)
/// The item expires after a given date.
case date(Date)
/// Indicates the item is already expired. Use this to skip cache.
case expired
}

ExpirationExtending用来表示再次访问后,如何更新该文件的缓存策略。

1
2
3
4
5
6
7
8
public enum ExpirationExtending {
/// The item expires after the original time, without extending after access.
case none
/// The item expiration extends by the original cache time after each access.
case cacheTime
/// The item expiration extends by the provided time after each access.
case expirationTime(_ expiration: StorageExpiration)
}

内存缓存

kingfisher内存缓存MemoryStorage定义了一个命名空间,里面有三个对象,Config,StorageObject和实际用来做处理的Backend。

StorageObject包括数据value,过期策略,和缓存的key值。

1
2
3
4
5
class StorageObject<T> {
let value: T
let expiration: StorageExpiration
let key: String
}

缓存配置对象

1
2
3
4
5
6
7
8
9
10
11
12
13
extension MemoryStorage {
/// Represents the config used in a `MemoryStorage`.
public struct Config {
//缓存容量上限
public var totalCostLimit: Int
//缓存数量上限
public var countLimit: Int = .max
//过期策略
public var expiration: StorageExpiration = .seconds(300)
//清理周期
public let cleanInterval: TimeInterval
}
}

实际做内存缓存处理的类Backend,以NSCache为存储源,将需要存储的数据构造成StorageObject来存储。提供存储,移除,查询三个功能,在查到缓存的时候,会根据延长缓存策略,更新这个对象的缓存策略。

1
2
3
4
5
6
7
8
9
10
func value(forKey key: String, extendingExpiration: ExpirationExtending = .cacheTime) -> T? {
guard let object = storage.object(forKey: key as NSString) else {
return nil
}
if object.expired {
return nil
}
object.extendExpiration(extendingExpiration)
return object.value
}

此外,会创建一个Timer,每隔一段时间(config中配置的)清理一次NSCache中过期的对象。

对NSCache的操作,都需要加锁,这里使用的NSLock。

磁盘缓存

磁盘缓存的结构跟内存缓存类似,使用DiskStorage枚举构造一个命名空间。内部定义了三个结构,配置信息Config,代表磁盘文件的FileMeta,以及处理逻辑的Backend。

1
2
3
4
5
6
7
8
9
10
public struct Config {
//文件大小上限
public var sizeLimit: UInt
//过期策略
public var expiration: StorageExpiration = .days(7)
//文件后缀
public var pathExtension: String? = nil
//是否使用hash值来表示文件名
public var usesHashedFileName = true
}
1
2
3
4
5
6
7
struct FileMeta {
let url: URL
let lastAccessDate: Date?
let estimatedExpirationDate: Date?
let isDirectory: Bool
let fileSize: Int
}

磁盘缓存的数据源是磁盘,在查询,删除,存储上,比内存缓存要麻烦一点。

Backend<T: DataTransformable>这里T是内部使用的数据类型。DataTransformable协议定义了T与Data的变换。需要存储时,要从T类型中拿到Data。下面看下store方法。

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
func store(
value: T,
forKey key: String,
expiration: StorageExpiration? = nil) throws
{
//缓存失效策略
let expiration = expiration ?? config.expiration
guard !expiration.isExpired else { return }

//拿到数据Data
let data: Data
do {
data = try value.toData()
} catch {
throw KingfisherError.cacheError(reason: .cannotConvertToData(object: value, error: error))
}
//根据key构建存储的url
let fileURL = cacheFileURL(forKey: key)

//构造文件属性:创建时间,更新时间(其实是过期时间)
let now = Date()
let attributes: [FileAttributeKey : Any] = [
// The last access date.
.creationDate: now.fileAttributeDate,
// The estimated expiration date.
.modificationDate: expiration.estimatedExpirationSinceNow.fileAttributeDate
]
//创建文件
config.fileManager.createFile(atPath: fileURL.path, contents: data, attributes: attributes)
}

存储文件带的属性,将创建时间和过期时间放进去。方便后续的处理。

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
func value(forKey key: String, referenceDate: Date, actuallyLoad: Bool) throws -> T? {
let fileManager = config.fileManager
let fileURL = cacheFileURL(forKey: key)
let filePath = fileURL.path
guard fileManager.fileExists(atPath: filePath) else {
return nil
}

//获取文件的meta信息,主要是创建时间,和过期时间
let meta: FileMeta
do {
let resourceKeys: Set<URLResourceKey> = [.contentModificationDateKey, .creationDateKey]
meta = try FileMeta(fileURL: fileURL, resourceKeys: resourceKeys)
} catch {
throw KingfisherError.cacheError(
reason: .invalidURLResource(error: error, key: key, url: fileURL))
}
//过期则返回nil
if meta.expired(referenceDate: referenceDate) {
return nil
}
//actuallyLoad为false,表明是为了查询,并不需要加载这个数据
if !actuallyLoad { return T.empty }

do {
//读取数据,修改过期时间
let data = try Data(contentsOf: fileURL)
let obj = try T.fromData(data)
metaChangingQueue.async { meta.extendExpiration(with: fileManager) }
return obj
} catch {
throw KingfisherError.cacheError(reason: .cannotLoadDataFromDisk(url: fileURL, error: error))
}
}

磁盘缓存没有定时清理。App将要进入后台,被杀死和被挂起的时候,会做一次清理。

文件操作

文件写入

1
2
3
4
5
6
7
8
9
let now = Date()
let attributes: [FileAttributeKey : Any] = [
// The last access date.
.creationDate: now.fileAttributeDate,
// The estimated expiration date.
.modificationDate: expiration.estimatedExpirationSinceNow.fileAttributeDate
]
//创建文件
config.fileManager.createFile(atPath: fileURL.path, contents: data, attributes: attributes)

缓存的文件写入是通过FileMananger的open func createFile(atPath path: String, contents data: Data?, attributes attr: [FileAttributeKey : Any]? = nil) -> Bool方法写入,在创建时间和修改时间属性中填入当前时间和过期时间。后续可以根据过期时间来清理磁盘。

文件属性的操作

读取文件属性可以通过url的resourceValues方法,如下

1
2
let resourceKeys: Set<URLResourceKey> = [.contentModificationDateKey, .creationDateKey]
let meta = try fileURL.resourceValues(forKeys: resourceKeys)

文件的属性很多,需要了解更多的,可以查看URLResourceKey这个结构体。库中主要用到了

1
2
3
4
5
6
7
8
//修改时间,实际存储的是过期时间
contentModificationDateKey
//创建时间
creationDateKey
//文件大小
fileSizeKey
//是否是目录
isDirectoryKey

文件属性的修改则是通过FileManager来处理

1
2
3
4
5
let attributes: [FileAttributeKey : Any] = [
.creationDate: Date().fileAttributeDate,
.modificationDate: .estimatedExpirationSinceNow.fileAttributeDate
]
try? fileManager.setAttributes(attributes, ofItemAtPath: url.path)

文件数据的读入,比较简单

1
let data = try Data(contentsOf: fileURL)

ImageCache

ImageCache包装了磁盘缓存和内存缓存,对外提供简单的查询,存储,清除等功能。

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
store(_ image: Image,
original: Data? = nil,
forKey key: String,
options: KingfisherParsedOptionsInfo,
toDisk: Bool = true,
completionHandler: ((CacheStoreResult) -> Void)? = nil)
{
let identifier = options.processor.identifier
let callbackQueue = options.callbackQueue
//创建一个key,用于缓存的键值
let computedKey = key.computedKey(with: identifier)
//使用内存缓存存储
memoryStorage.storeNoThrow(value: image, forKey: computedKey, expiration: options.memoryCacheExpiration)

guard toDisk else {
//如果不需要存磁盘,这里就算存储成功里,构建callback
if let completionHandler = completionHandler {
let result = CacheStoreResult(memoryCacheResult: .success(()), diskCacheResult: .success(()))
callbackQueue.execute { completionHandler(result) }
}
return
}
//io专用的队列,异步将data存入磁盘
ioQueue.async {
let serializer = options.cacheSerializer
//将数据序列化
if let data = serializer.data(with: image, original: original) {
//调用同步存磁盘的方法,方法的实现是调用磁盘缓存的store方法
self.syncStoreToDisk(
data,
forKey: key,
processorIdentifier: identifier,
callbackQueue: callbackQueue,
expiration: options.diskCacheExpiration,
completionHandler: completionHandler)
} else {
guard let completionHandler = completionHandler else { return }

let diskError = KingfisherError.cacheError(
reason: .cannotSerializeImage(image: image, original: original, serializer: serializer))
let result = CacheStoreResult(
memoryCacheResult: .success(()),
diskCacheResult: .failure(diskError))
callbackQueue.execute { completionHandler(result) }
}
}
}

流程介绍

Kingfisher最常用的方式

1
2
let url = URL(string: "https://example.com/image.png")
imageView.kf.setImage(with: url)

入口即setImage,从这个方法进去看看

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
@discardableResult
public func setImage(
with source: Source?,
placeholder: Placeholder? = nil,
options: KingfisherOptionsInfo? = nil,
progressBlock: DownloadProgressBlock? = nil,
completionHandler: ((Result<RetrieveImageResult, KingfisherError>) -> Void)? = nil) -> DownloadTask?
{
var mutatingSelf = self
//没有source直接返回
guard let source = source else {
mutatingSelf.placeholder = placeholder
mutatingSelf.taskIdentifier = nil
completionHandler?(.failure(KingfisherError.imageSettingError(reason: .emptySource)))
return nil
}
//处理options
var options = KingfisherParsedOptionsInfo(KingfisherManager.shared.defaultOptions + (options ?? .empty))
let noImageOrPlaceholderSet = base.image == nil && self.placeholder == nil
if !options.keepCurrentImageWhileLoading || noImageOrPlaceholderSet {
// Always set placeholder while there is no image/placeholder yet.
mutatingSelf.placeholder = placeholder
}
//如果有动画,启动动画
let maybeIndicator = indicator
maybeIndicator?.startAnimatingView()

//取自增值作为任务的Identifier
let issuedIdentifier = Source.Identifier.next()
mutatingSelf.taskIdentifier = issuedIdentifier

if base.shouldPreloadAllAnimation() {
options.preloadAllAnimationData = true
}

//进度block
if let block = progressBlock {
options.onDataReceived = (options.onDataReceived ?? []) + [ImageLoadingProgressSideEffect(block)]
}

if let provider = ImageProgressiveProvider(options, refresh: { image in
self.base.image = image
}) {
options.onDataReceived = (options.onDataReceived ?? []) + [provider]
}

options.onDataReceived?.forEach {
$0.onShouldApply = { issuedIdentifier == self.taskIdentifier }
}
//通过KingfisherManager来获取图片
let task = KingfisherManager.shared.retrieveImage(
with: source,
options: options,
completionHandler: { result in
CallbackQueue.mainCurrentOrAsync.execute {
maybeIndicator?.stopAnimatingView()
//如果拿到图片后,任务已经不是当前任务了,就不处理
guard issuedIdentifier == self.taskIdentifier else {
let reason: KingfisherError.ImageSettingErrorReason
do {
let value = try result.get()
reason = .notCurrentSourceTask(result: value, error: nil, source: source)
} catch {
reason = .notCurrentSourceTask(result: nil, error: error, source: source)
}
let error = KingfisherError.imageSettingError(reason: reason)
completionHandler?(.failure(error))
return
}

mutatingSelf.imageTask = nil
mutatingSelf.taskIdentifier = nil
switch result {
//数据拿到设置image
case .success(let value):
guard self.needsTransition(options: options, cacheType: value.cacheType) else {
mutatingSelf.placeholder = nil
self.base.image = value.image
completionHandler?(result)
return
}

self.makeTransition(image: value.image, transition: options.transition) {
completionHandler?(result)
}
//任务失败
case .failure:
if let image = options.onFailureImage {
self.base.image = image
}
completionHandler?(result)
}
}
}
)
mutatingSelf.imageTask = task
return task
}

流程大概是:

  • UIImageView调用setImage设置图片
  • 通过KingfisherManager.shared.retrieveImage获取图片
  • KingfisherManager.shared.retrieveImage去查内存缓存,有就返回,没有继续
  • KingfisherManager.shared.retrieveImage去查磁盘缓存,有就返回,没有继续
  • KingfisherManager.shared.loadAndCacheImage下载并缓存图片
  • 查看图片中当前的任务和刚执行完的任务是不是同一个,同一个就设置图片,结束
  • 不是同一个,就不处理,结束

UIButton的setImage是类似的逻辑,就不多说了

这里需要特别说明的是在tableView中的处理,因为tableView中,快速上下滑动,cell重用。会导致一个UIImageView多次setImage。上面流程中的回设图片中会对比当前task和完成的task,如果不一致,说明这个是之前的某次setImage开启的下载完成了。这是一个对重用的处理细节。

另外当重新滑到之前的位置,setImage的url是开启的,并且没有下载完成,缓存中找不到。这个时候也不会立刻开始下载,而是去看下载任务队列中是否有该url的任务,找到之后,将这个任务绑定到这个UIImageView上。这也是重用的一个场景。

多线程处理

库中会频繁用到网络请求和磁盘读写,为了不阻塞主线程,很显然需要用到多线程处理。使用URLSession,数据的返回默认会从异步执行。而磁盘的读写则需要我们自己处理,库中创建了一个处理IO的串行队列来处理。

因为这些异步处理,需要一些手段保证数据的一致性。下面列举一下临界区。

KingfisherManager是一个单例,所有UIImageView,UIButton的setImage操作都是通过它来完成。KingfisherManager中有一个ImageCache和一个downloader。cache中的内存缓存是一个NSCache对象,全局的存取到会用到。

downloader中下载会返回一个task,这些task会在sessionDelegate中管理。private var tasks: [URL: SessionDataTask] = [:]

SessionDataTask中保存了该任务的回调信息

多线程会导致一些同步问题,从网络下载完图片,会在未知线程缓存,接下来缓存时,有可能遇到其他任务正在读取缓存的情况。所以内存缓存的写入时加锁处理的。

库中多用队列来处理任务。流程中可能出现的多线程问题:

  • 同个文件的读写

请求的起点一般是从UIImageView.setImage开始的,这个方法的调用一般是在主线程中发起。这个方法拿到数据后,completionHandler是通过CallbackQueue.mainCurrentOrAsync在主线程中执行。

在获取数据,会先查内存缓存,在查磁盘缓存。内存缓存的查询比较快,会在当前线程知节执行。磁盘缓存的查询,会使用一个单独的串行队列完成。

补充

Delegate<Input, Output>

Kingfisher 中使用Delegate类来包装block,添加[weak self]来避免 retain cycle。

1
2
3
4
5
6
7
8
9
10
11
12
13
class Delegate<Input, Output> {
init() {}
private var block: ((Input) -> Output?)?
func delegate<T: AnyObject>(on target: T, block: ((T, Input) -> Output)?) {
self.block = { [weak target] input in
guard let target = target else { return nil }
return block?(target, input)
}
}
func call(_ input: Input) -> Output? {
return block?(input)
}
}

delegate方法,将传入的block包装成自己的block,中间加入weak处理。call方法则是调用这个block。

defer的使用

1
2
3
4
5
6
7
func addCallback(_ callback: TaskCallback) -> CancelToken {
lock.lock()
defer { lock.unlock() }
callbacksStore[currentToken] = callback
defer { currentToken += 1 }
return currentToken
}

第二个defer调用之后,返回的currentToken,值是+1之前的。所以deffer的调用时机,其实是返回之后。

从语言设计上来说,defer 的目的就是进行资源清理和避免重复的返回前需要执行的代码,而不是用来以取巧地实现某些功能。这样做只会让代码可读性降低。

上边这段话是作者在关于 Swift defer 的正确使用中说的话,然后还是在kingfisher中用了。一个小趣点。

另外这篇文章中还提到了作用域的问题,defer这个词,并不是在方法返回后执行的。而是所在的作用域结束后返回的。比如你在if语句中写defer,那这个defer就是在if之后执行。不是在方法返回之后。文中有具体例子,可以细看。

这里使用了多个defer,之前没见过,查了下,多个defer的调用会以反序执行,类似入栈,出栈,网上有个很不错的例子,这里看一下。

1
2
3
4
5
guard let database = openDatabase(...) else { return }
defer { closeDatabase(database) }
guard let connection = openConnection(database) else { return }
defer { closeConnection(connection) }
guard let result = runQuery(connection, ...) else { return }

打开一个数据库,打开一个连接,结束之后,defer反序执行,先关掉连接,在关数据库。很恰当。

GCD的使用

库中对GCD的使用,是包装了一个CallbackQueue。

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
public enum CallbackQueue {
case mainAsync
case mainCurrentOrAsync
case untouch
case dispatch(DispatchQueue)

public func execute(_ block: @escaping () -> Void) {
switch self {
case .mainAsync:
DispatchQueue.main.async { block() }
case .mainCurrentOrAsync:
DispatchQueue.main.safeAsync { block() }
case .untouch:
block()
case .dispatch(let queue):
queue.async { block() }
}
}

var queue: DispatchQueue {
switch self {
case .mainAsync: return .main
case .mainCurrentOrAsync: return .main
case .untouch: return OperationQueue.current?.underlyingQueue ?? .main
case .dispatch(let queue): return queue
}
}
}

从execute的实现可以了解这几个枚举的含义。有一个是没出现过的,就是safeAsync。

1
2
3
4
5
6
7
8
9
10
11
12
extension DispatchQueue {
// This method will dispatch the `block` to self.
// If `self` is the main queue, and current thread is main thread, the block
// will be invoked immediately instead of being dispatched.
func safeAsync(_ block: @escaping ()->()) {
if self === DispatchQueue.main && Thread.isMainThread {
block()
} else {
async { block() }
}
}
}

这个safeAsync里会先判断当前是不是主队列和主线程。如果是主线程,主队列,就直接执行block,不然就async执行该block。这个方法只在上面的mainCurrentOrAsync由主队列调用,感觉这样写不是特别好,或者名字不是特别恰当。因为这个方法是加载DispatchQueue上的,所以其他队列也是可以调用的,但是非主队列调用,都是有问题的。这个方法的声明的范围太广了。

rsa加密

Posted on 2019-06-26 | Edited on 2019-06-28

非对称加密算法 RSA(三位作者的名字首字母)

区别于对称加密算法,非对称加密算法采用两种密钥,公钥和私钥,公钥加密,私钥解密,反之也可以。私钥保留,公钥可以公开发布。

这种算法非常可靠,密钥越长,它就越难破解。根据已经披露的文献,目前被破解的最长RSA密钥是768个二进制位。也就是说,长度超过768位的密钥,还无法破解(至少没人公开宣布)。因此可以认为,1024位的RSA密钥基本安全,2048位的密钥极其安全。

RSA生成密钥的过程用到欧拉函数,模反元素等,十分复杂。

  • 随机选择两个不相等的质数p和q
  • 计算p和q的乘积n,上边说的密钥长度,就是n的长度
  • 计算n的欧拉函数φ(n)
  • 随机选择一个整数e,条件是1< e < φ(n),且e与φ(n) 互质
  • 计算e对于φ(n)的模反元素d

过程虽然复杂,但是一般也不需要特别清楚,我们只需知道计算过程用到了p,q,n,e,d,这些值封装在一起就是私钥,其中n和e封装在一起就是公钥。对计算过程感兴趣的可以看看阮一峰的博客RSA算法原理(一)和RSA算法原理(二)。

我们的数据,经过公钥中的n和e进行计算。计算出来的值,可以通过私钥中的n和d反解出来。

密钥的形式

ASN.1

密钥中数据采用ASN.1结构。ASN.1本身只定义了表示信息的抽象句法,但是没有限定其编码的方法。ASN.1有各种编码规则,其中密钥采用唯一编码规则(DER,Distinguished Encoding Rules)。用ASN.1表示法,公私钥大概是如下形式。

PublicKey

1
2
3
4
RSAPublicKey ::= SEQUENCE {
modulus INTEGER, – n
publicExponent INTEGER – e
}

PrivateKey

1
2
3
4
5
6
7
8
9
10
11
12
RSAPrivateKey ::= SEQUENCE {
version Version,
modulus INTEGER, -- n
publicExponent INTEGER, -- e
privateExponent INTEGER, -- d
prime1 INTEGER, -- p
prime2 INTEGER, -- q
exponent1 INTEGER, -- d mod (p-1)
exponent2 INTEGER, -- d mod (q-1)
coefficient INTEGER, -- (inverse of q) mod p
otherPrimeInfos OtherPrimeInfos OPTIONAL
}

DER 编码介绍

DER编码会将数据编码成二机制形式。DER使用一种TLV格式来描述数据

如果tag是容器类型,value就是另一组TLV了

tag编码

tag一般占一个字节。前两位表示class类型,第三位表示原子类型还是结构体类型。

具体类型编码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
0x01 == BOOLEAN
0x02 == Integer
0x03 == Bit String
0x04 == Octet String
0x05 == NULL
0x06 == Object Identifier

0x0C == UTF8String
0x13 == PrintableString
0x14 == TeletexString
0x16 == IA5String
0x1E == BMPString

0x30 == SEQUENCE or SEQUENCE OF
0x31 == SET or SET OF

说明:所有的这些都是UNIVERSAl类型。因为前两位都是0。1E之前的都是primitive类型,因为第三位都是0。30和31是constructed类型,第三位是1。

长度的约定

长度字段标示value字段的长度。如果value字段小于128字节,长度字段占一字节。并且字节第一位是0。如果value字段多于128个字节。那这一字节的第一位设置为1,接下来的位数表示需要几个字节来表示长度字段。

说明:为什么以128为界。因为默认情况下以一个字节表达长度,而这个字节的第一位用来标志是否溢出。所以只剩下7位用来表达长度。7位可以表达的最大数字是2的7次方128。所以以128为界。如果不用第一位来标志溢出,虽然可以表达256的长度,但是超过256就无法表达了。

值的约定

每个值都有一些特殊说明,全部列举比较繁杂,密钥中主要是用INTEGER类型和SEQUENCE类型,下面我们对这两种值类型进行一些说明。

INTEGER

正常情况指定长度中的值就是编码后的值。

比如: 0x03,编码后为 0x02 0x01 0x03。类型,长度,值,很容易理解。

但是当一个正数,并且第一位是1的时候,需要做一些标示,来表明这个值是正值。标志很简单,就是在值前面加一个全0字节。

比如值 0x8F(10001111)首位是1

0x02表明类型是INTEGER,长度两个字节,0x00表示是正数。0x8F就是这个值。

SEQUENCE

SEQUENCE 包含一组有序的值。超过128位的情况,按照之前的长度约定走。例子如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
30 81 9f                             ; SEQUENCE (9f Bytes)
| 30 0d ; SEQUENCE (d Bytes)
| | | 06 09 ; OBJECT_ID (9 Bytes)
| | | 2a 86 48 86 f7 0d 01 01 01 ; 1.2.840.113549.1.1.1
| | 05 00 ; NULL (0 Bytes)
| 03 81 8d ; BIT_STRING (8d Bytes)
| 00
| 30 81 89 ; SEQUENCE (89 Bytes)
| 02 81 81 ; INTEGER (81 Bytes)
| | 00
| | 8f e2 41 2a 08 e8 51 a8 8c b3 e8 53 e7 d5 49 50
| | b3 27 8a 2b cb ea b5 42 73 ea 02 57 cc 65 33 ee
| | 88 20 61 a1 17 56 c1 24 18 e3 a8 08 d3 be d9 31
| | f3 37 0b 94 b8 cc 43 08 0b 70 24 f7 9c b1 8d 5d
| | d6 6d 82 d0 54 09 84 f8 9f 97 01 75 05 9c 89 d4
| | d5 c9 1e c9 13 d7 2a 6b 30 91 19 d6 d4 42 e0 c4
| | 9d 7c 92 71 e1 b2 2f 5c 8d ee f0 f1 17 1e d2 5f
| | 31 5b b1 9c bc 20 55 bf 3a 37 42 45 75 dc 90 65
| 02 03 ; INTEGER (3 Bytes)
| 01 00 01

外层SEQUENCE,81表示接下来1个字节表示长度,接下来的字节是9f,所以这个值占9f个字节。

内层SEQUENCE,0d,表示接下来d个字节是内容。

密钥的表现形式

接下来的分析,主要依据上边的编码介绍,可以对照着看。

密钥一般不是直接以der形式存在,大部分情况都会进行二次编码。比如下面这三个例子。

openssl生成的私钥

使用openssl生成一个私钥

1
openssl genrsa -out private_rsa.pem  1024

使用base64解码,生成一个二机制文件

1
openssl   base64  -d  -in private_rsa.pem -out private

可以用vim查看,

1
vi -b private

在vim中,将展示改成16进制:%!xxd

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
00000000: 3082 025d 0201 0002 8181 00cc 9cd8 3a75  0..]..........:u
00000010: 7080 3935 302a 3299 645c abbe 71b5 c3dd p.950*2.d\..q...
00000020: 5e3e 4421 409c 29ff 58c8 ff80 b2e5 6393 ^>D!@.).X.....c.
00000030: 13fe 6576 9f7f be6b 7c3a 80f6 0645 9bfc ..ev...k|:...E..
00000040: d878 6db5 8022 d929 2492 3f6c e69c 5603 .xm..".)$.?l..V.
00000050: 46b0 60b1 69a7 4de8 ff40 7061 dc22 b279 F.`.i.M..@pa.".y
00000060: c5b1 ccc5 d695 72cb 665f eada 0bed 9d27 ......r.f_.....'
00000070: 6e22 66e1 4d51 b3be 3393 2806 ea12 8927 n"f.MQ..3.(....'
00000080: 454c e3a4 a9ca a5b3 80c7 ad02 0301 0001 EL..............
00000090: 0281 800f 179a 9365 4a31 0b07 3350 497f .......eJ1..3PI.
000000a0: 2af9 f2e9 0f36 1b06 5f07 34bb 472a bda6 *....6.._.4.G*..
000000b0: 4a04 3964 62cd acb4 928a f72c f2c2 d766 J.9db......,...f
000000c0: d238 f67e 2f24 3f47 3d28 54df 485e 49aa .8.~/$?G=(T.H^I.
000000d0: 513a 4035 a265 02bd eb99 027a 214c 3a32 Q:@5.e.....z!L:2
000000e0: 343b e023 77d2 3c24 4af1 6d12 1c79 5190 4;.#w.<$J.m..yQ.
000000f0: 1cbf 73a9 32b8 e9a7 1daa 5382 2f91 7c2d ..s.2.....S./.|-
00000100: b440 2c0d 31a4 85ee 72bb 7eae 03fb b895 .@,.1...r.~.....
00000110: e812 8102 4100 e708 c31b 59bd 52cd fe3e ....A.....Y.R..>
00000120: 130e ec2b 0b30 b983 17d3 843b d483 ba07 ...+.0.....;....
00000130: b46a 911c 9f7e 6df5 9cdc cc04 dfe9 d5cf .j...~m.........
00000140: 6e8e 7401 5e8d b5d4 cb41 4fa9 1093 d8c4 n.t.^....AO.....
00000150: de0d 4b1e 51b1 0241 00e2 b92a d49d 8ccf ..K.Q..A...*....
00000160: bf3a f78e 2133 c6db bac4 6af5 4414 d514 .:..!3....j.D...
00000170: 7791 6491 b2e3 a1ca 91e1 d88f 6f1e 1f25 w.d.........o..%
00000180: d42f deb6 5b3e 85c7 b467 df10 7040 3877 ./..[>...g..p@8w
00000190: 205f 3059 c3da 6bf8 bd02 4046 9520 b65c _0Y..k...@F. .\
000001a0: 6640 c3fa 2690 c000 5aee 2246 aacc 3eac f@..&...Z."F..>.
000001b0: a972 b583 c212 d673 dae0 c749 64be 359e .r.....s...Id.5.
000001c0: 86e6 b993 beb9 b1ff b2e3 663b e4f4 ebd1 ..........f;....
000001d0: 207f 960b a5a9 893a 27db 2102 4100 9527 ......:'.!.A..'
000001e0: b258 bbe9 8646 cd59 4d64 e476 3fda 281c .X...F.YMd.v?.(.
000001f0: 218d 0f93 7aea 8a79 3a2d 10fa 4095 269a !...z..y:-..@.&.
00000200: 5d0a 822b 85ac 896d a054 78d6 7422 686f ]..+...m.Tx.t"ho
00000210: 6496 2479 c14d 47b2 3c6b cfc7 5695 0241 d.$y.MG.<k..V..A
00000220: 00a9 9a8c 20ac a6fb 5a17 8df8 e118 397d .... ...Z.....9}
00000230: 22bc 706a 47cb 9d11 2f4d a4e5 708c 044d ".pjG.../M..p..M
00000240: 1f4a 6870 3365 a8df 0c1b 440f 12c7 86de .Jhp3e....D.....
00000250: 4246 0b99 3c9e f01f 2313 b0bf 894b aaa1 BF..<...#....K..
00000260: 58

接下来比较长,比较乏味,我将生成的私钥进行了简单的格式处理,我们看看是不是符合ASN.1中对私钥的描述

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
# VERSION
0201 00

# modulus INTEGER, -- n
02 8181
00cc 9cd8 3a75
7080 3935 302a 3299 645c abbe 71b5 c3dd 5e3e 4421 409c 29ff 58c8 ff80 b2e5 6393
13fe 6576 9f7f be6b 7c3a 80f6 0645 9bfc d878 6db5 8022 d929 2492 3f6c e69c 5603
46b0 60b1 69a7 4de8 ff40 7061 dc22 b279 c5b1 ccc5 d695 72cb 665f eada 0bed 9d27
6e22 66e1 4d51 b3be 3393 2806 ea12 8927 454c e3a4 a9ca a5b3 80c7 ad

# publicExponent INTEGER, -- e 65537
02 0301 0001

# privateExponent INTEGER, -- d
0281 80
0f 179a 9365 4a31 0b07 3350 497f 2af9 f2e9 0f36 1b06 5f07 34bb 472a bda6
4a04 3964 62cd acb4 928a f72c f2c2 d766 d238 f67e 2f24 3f47 3d28 54df 485e 49aa
513a 4035 a265 02bd eb99 027a 214c 3a32 343b e023 77d2 3c24 4af1 6d12 1c79 5190
1cbf 73a9 32b8 e9a7 1daa 5382 2f91 7c2d b440 2c0d 31a4 85ee 72bb 7eae 03fb b895 e812 81

# prime1 INTEGER, -- p
02 41
00
e708 c31b 59bd 52cd fe3e 130e ec2b 0b30 b983 17d3 843b d483 ba07
b46a 911c 9f7e 6df5 9cdc cc04 dfe9 d5cf 6e8e 7401 5e8d b5d4 cb41 4fa9 1093 d8c4
de0d 4b1e 51b1

# prime2 INTEGER, -- q
0241
00
e2 b92a d49d 8ccf bf3a f78e 2133 c6db bac4 6af5 4414 d514
7791 6491 b2e3 a1ca 91e1 d88f 6f1e 1f25 d42f deb6 5b3e 85c7 b467 df10 7040 3877
205f 3059 c3da 6bf8 bd

# exponent1 INTEGER, -- d mod (p-1)
02 40
46 9520 b65c
6640 c3fa 2690 c000 5aee 2246 aacc 3eac a972 b583 c212 d673 dae0 c749 64be 359e
86e6 b993 beb9 b1ff b2e3 663b e4f4 ebd1 207f 960b a5a9 893a 27db 21

# exponent2 INTEGER, -- d mod (q-1)
02 41
00
9527 b258 bbe9 8646 cd59 4d64 e476 3fda 281c 218d 0f93 7aea 8a79 3a2d 10fa 4095 269a
5d0a 822b 85ac 896d a054 78d6 7422 686f 6496 2479 c14d 47b2 3c6b cfc7 5695

# coefficient INTEGER, -- (inverse of q) mod p
0241
00
a9 9a8c 20ac a6fb 5a17 8df8 e118 397d 22bc 706a 47cb 9d11 2f4d a4e5 708c 044d
1f4a 6870 3365 a8df 0c1b 440f 12c7 86de 4246 0b99 3c9e f01f 2313 b0bf 894b aaa1 58

可以看到,私钥中的数据在这里面有完整的对应。

服务端给的公钥,待补充:这个公钥的类型是什么?
1
2
3
4
5
6
-----BEGIN PUBLIC KEY-----
MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCBzf/UKYSGEZNn0ziH8ZhcSmIJ
bVIzGx95BW2URGzpuNDUbdX55mOMPO9Arw/j5zh1kPG5fVq0BcZMFkYhIOn4+6kj
awVpjnNzCvvj2//csftaKyyFslvKPf1Gu3kd/OTVKg93L0kL+SFmPtqI1RT6HUqK
4N6Ht24bia11kkgnewIDAQAB
-----END PUBLIC KEY-----

格式化后

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
3081 9f //9f长度的SEQUENCE数据
30 0d //d 长度的SEQUENCE数据
06 09 2a 8648 86f7 0d01 0101 //9 长度的 Object Identifier数据 1.2.840.113549.1.1.1
0500 //结尾的NULL
0381 8d //8d长度的 Bit String 数据
00
3081 8902 8181 0081 cdff
d429 8486 1193 67d3 3887 f198 5c4a 6209
6d52 331b 1f79 056d 9444 6ce9 b8d0 d46d
d5f9 e663 8c3c ef40 af0f e3e7 3875 90f1
b97d 5ab4 05c6 4c16 4621 20e9 f8fb a923
6b05 698e 7373 0afb e3db ffdc b1fb 5a2b
2c85 b25b ca3d fd46 bb79 1dfc e4d5 2a0f
772f 490b f921 663e da88 d514 fa1d 4a8a
e0de 87b7 6e1b 89ad 7592 4827 7b02 0301
00 01

头信息很长,首先是一个SEQUNENCE开头,里面有一个SEQUNENCE和一个Bit String。

内部的这个SEQUNENCE包装了两个数据,一个对象标识,一个空数据结尾,这个对象标识解出来是1.2.840.113549.1.1.1,对应的名字是szOID_RSA_RSA。这是加密算法标识符,有很多类型,这个类型表示,RSA既可以用于加密,也可以用于给数据签名。详细信息可以参考。

Bit String中放的数据就是我们的公钥了,下面看看这个公钥

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
30 81 89

02 81 81
0081 cdff
d429 8486 1193 67d3 3887 f198 5c4a 6209
6d52 331b 1f79 056d 9444 6ce9 b8d0 d46d
d5f9 e663 8c3c ef40 af0f e3e7 3875 90f1
b97d 5ab4 05c6 4c16 4621 20e9 f8fb a923
6b05 698e 7373 0afb e3db ffdc b1fb 5a2b
2c85 b25b ca3d fd46 bb79 1dfc e4d5 2a0f
772f 490b f921 663e da88 d514 fa1d 4a8a
e0de 87b7 6e1b 89ad 7592 4827 7b

02 03
01 00 01

以SEQUNENCE作为容器,里面有两个Integer,一个是129个字节的n,一个是e 65537。

sshkeygen生成的公钥
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
00000000: 0000 0007 7373 682d 7273 6100 0000 0301  ....ssh-rsa.....
00000010: 0001 0000 0101 00a0 f5d6 20a3 2ff7 6106 .......... ./.a.
00000020: d301 e98a d7a8 6307 bc30 74e8 5668 d7b0 ......c..0t.Vh..
00000030: 80a8 0f8d c1c0 c495 53f4 e016 ec17 26d1 ........S.....&.
00000040: 1635 bfe6 0a60 6174 1a9c dcef 176e 0fb7 .5...`at.....n..
00000050: bd66 a939 25ce 1a56 c446 e3c5 1c31 b894 .f.9%..V.F...1..
00000060: c17f db57 bd4e 88bc b3a8 5c95 919c 2394 ...W.N....\...#.
00000070: 39d0 254b ecac 94fb 7d69 ec1c 143a 434f 9.%K....}i...:CO
00000080: d6a8 4572 eaf6 aef3 3d5e 7899 4ba5 739d ..Er....=^x.K.s.
00000090: 45d6 d928 b506 93fd 41ae d3f8 1149 92ac E..(....A....I..
000000a0: f483 a853 22cc ac09 ee5f 76ff 36b6 0424 ...S"...._v.6..$
000000b0: 2dce 9ea1 be1b 92aa 73b6 f1cf 22dc f304 -.......s..."...
000000c0: e146 50ad 472d 4885 9d67 160c fac7 b2e8 .FP.G-H..g......
000000d0: 915b 925c 979f 54b3 2934 269d 28e2 e88b .[.\..T.)4&.(...
000000e0: 202d c95c a8c8 66af 5784 6cf2 269e abc6 -.\..f.W.l.&...
000000f0: 1bf0 5531 66a4 72fc d10b 9237 00e0 9b17 ..U1f.r....7....
00000100: d2ab 7bf0 a135 0b37 d399 6731 eb83 6f2e ..{..5.7..g1..o.
00000110: a49c 84c0 9781 63

这个有点尴尬,好像不是der的编码格式。而且,第一行还显示出了ssh-rsa。这个格式没找到对应的编码介绍,网上有人翻译,看翻译过程,这个编码格式像是简化了的der。可能因为信息类型确定,长度也相对确定,所以省掉了tag,以及长度溢出的措施,这里应该是LV格式,即长度-值。长度占4个字节。我们用这个规则看一下上面的数据。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
0000 0007 //长度 7
7373 682d 7273 61 //ascii码:ssh-rsa
0000 0003 //长度 3
010001 // e 65537
0000 0101 //长度256
//下面的256个字节,n
00 a0 f5d6 20a3 2ff7 6106
d301 e98a d7a8 6307 bc30 74e8 5668 d7b0
80a8 0f8d c1c0 c495 53f4 e016 ec17 26d1
1635 bfe6 0a60 6174 1a9c dcef 176e 0fb7
bd66 a939 25ce 1a56 c446 e3c5 1c31 b894
c17f db57 bd4e 88bc b3a8 5c95 919c 2394
39d0 254b ecac 94fb 7d69 ec1c 143a 434f
d6a8 4572 eaf6 aef3 3d5e 7899 4ba5 739d
45d6 d928 b506 93fd 41ae d3f8 1149 92ac
f483 a853 22cc ac09 ee5f 76ff 36b6 0424
2dce 9ea1 be1b 92aa 73b6 f1cf 22dc f304
e146 50ad 472d 4885 9d67 160c fac7 b2e8
915b 925c 979f 54b3 2934 269d 28e2 e88b
202d c95c a8c8 66af 5784 6cf2 269e abc6
1bf0 5531 66a4 72fc d10b 9237 00e0 9b17
d2ab 7bf0 a135 0b37 d399 6731 eb83 6f2e
a49c 84c0 9781 63

这个数据表达了三个信息,类型ssh-rsa,e 65537,n那一大串数。

ssh-keygen生成的公钥id_rsa.pub,可以通过命令ssh-keygen -f key.pub -e -m pem转成标准der格式。

比如上面那个公钥经过这个命令之后是这样的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
00000000: 3082 010a 0282 0101 00a0 f5d6 20a3 2ff7  0........... ./.
00000010: 6106 d301 e98a d7a8 6307 bc30 74e8 5668 a.......c..0t.Vh
00000020: d7b0 80a8 0f8d c1c0 c495 53f4 e016 ec17 ..........S.....
00000030: 26d1 1635 bfe6 0a60 6174 1a9c dcef 176e &..5...`at.....n
00000040: 0fb7 bd66 a939 25ce 1a56 c446 e3c5 1c31 ...f.9%..V.F...1
00000050: b894 c17f db57 bd4e 88bc b3a8 5c95 919c .....W.N....\...
00000060: 2394 39d0 254b ecac 94fb 7d69 ec1c 143a #.9.%K....}i...:
00000070: 434f d6a8 4572 eaf6 aef3 3d5e 7899 4ba5 CO..Er....=^x.K.
00000080: 739d 45d6 d928 b506 93fd 41ae d3f8 1149 s.E..(....A....I
00000090: 92ac f483 a853 22cc ac09 ee5f 76ff 36b6 .....S"...._v.6.
000000a0: 0424 2dce 9ea1 be1b 92aa 73b6 f1cf 22dc .$-.......s...".
000000b0: f304 e146 50ad 472d 4885 9d67 160c fac7 ...FP.G-H..g....
000000c0: b2e8 915b 925c 979f 54b3 2934 269d 28e2 ...[.\..T.)4&.(.
000000d0: e88b 202d c95c a8c8 66af 5784 6cf2 269e .. -.\..f.W.l.&.
000000e0: abc6 1bf0 5531 66a4 72fc d10b 9237 00e0 ....U1f.r....7..
000000f0: 9b17 d2ab 7bf0 a135 0b37 d399 6731 eb83 ....{..5.7..g1..
00000100: 6f2e a49c 84c0 9781 6302 0301 0001 o.......c.....

这个就很熟悉了,标准的TLV格式。SEQUNENCE下两个INTEGER,n和e。

待补充

密钥的格式只见到过这几种,就对这几种进行了分析。还需要看看总共有多少种,如何分类,以及为什么这么分类。

参考

解读RSA公钥私钥储存格式

stackoverflow

CONVERTING OPENSSH PUBLIC KEYS

DER 编码

RSA 算法原理

各类证书格式

iOS内存介绍

Posted on 2019-03-23 | Edited on 2019-08-07 | In iOS , 内存

概览

iOS 启动,系统将App装载进内存。然后将这块区域内部划分成如下部分。

图中与我们相关性较大的是栈区和堆区。栈区是为了实现方法的调用而存在的,内存管理由系统处理。堆区存放开发者创建的对象。通常我们说的内存管理,就是指的堆中的内存管理。

引用计数

iOS中采用引用计数的方式管理内存,每多一个强引用,引用计数就加一,销毁一个强引用,引用计数就减一。引用计数为0时,会触发dealloc方法,释放该对象。下面将要介绍的东西都是在引用技术基础上做的内存管理。

内存相关结构与操作

相关数据结构

引用计数涉及到的数据结构有isa指针,sidetables,sidetable,weaktable等。

目前iOS设备普遍是arm64系统,指针位数为64位,可以寻址2的64次方的内存,目前完全用不到。所以运行时库对isa指针进行了特殊的处理,下面是非指针类型的isa。只有shiftcls代表的33位是表示指针。

这里引用计数相关的有has_sidetable_rc和extra_rc,extra_rc是用来存储该对象被引用的次数的。但是因为位数有限,能表达的数量很有限,当不够表达时,has_sidetable_rc标记是否存储在sidetable中。

1
2
3
4
5
6
7
8
9
10
11
struct {
uintptr_t indexed : 1;
uintptr_t has_assoc : 1;
uintptr_t has_cxx_dtor : 1;
uintptr_t shiftcls : 33; // MACH_VM_MAX_ADDRESS 0x1000000000
uintptr_t magic : 6;
uintptr_t weakly_referenced : 1;
uintptr_t deallocating : 1;
uintptr_t has_sidetable_rc : 1;
uintptr_t extra_rc : 19;
};

sidetables由64个sidetable组成。通过hash算法定位。因为这些表是全局共享,会频繁的并发读写,如果只有一个表,多个线程同时操作时,要等很久。分表后可以大大减少多个线程同时操作一个表的情况,提高性能。

1
2
3
4
5
6
7
8
struct SideTable {
// 保证原子操作的自旋锁
spinlock_t slock;
// 引用计数的 hash 表
RefcountMap refcnts;
// weak 引用全局 hash 表
weak_table_t weak_table;
};

引用计数表以对象指针为key,以引用计数+两个标记位 为value。因为后两个标记位所以,当引用计数需要加减的时候,是从第三位开始。后面的两个标记位,一个是表示是否正在处于dealloc,一个表示是否有若引用,后面还会看到。

weak表以对象指针为key,以引用地址的数组为value,每增加一个weak引用,就添加到这个数组中。

retain,relase等相关的操作都是针对这些结构的添加修改删除。

操作

retainCount

retainCount比较简单,根据对象地址找到sidetable,然后继续在RefCountmap中找到计数并返回

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
uintptr_t
objc_object::sidetable_retainCount()
{
SideTable& table = SideTables()[this];

size_t refcnt_result = 1;

table.lock();
RefcountMap::iterator it = table.refcnts.find(this);
if (it != table.refcnts.end()) {
// this is valid for SIDE_TABLE_RC_PINNED too
refcnt_result += it->second >> SIDE_TABLE_RC_SHIFT;
}
table.unlock();
return refcnt_result;
}

retain
retain操作会对引用计数加1

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
id
objc_object::sidetable_retain()
{
#if SUPPORT_NONPOINTER_ISA
assert(!isa.indexed);
#endif
SideTable& table = SideTables()[this];

if (table.trylock()) {
size_t& refcntStorage = table.refcnts[this];
if (! (refcntStorage & SIDE_TABLE_RC_PINNED)) {
refcntStorage += SIDE_TABLE_RC_ONE;
}
table.unlock();
return (id)this;
}
return sidetable_retain_slow(table);
}

release

自动引用减一,减到0,会调用SEL_dealloc,触发dealloc。

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
uintptr_t 
objc_object::sidetable_release(bool performDealloc)
{
#if SUPPORT_NONPOINTER_ISA
assert(!isa.indexed);
#endif
SideTable& table = SideTables()[this];

bool do_dealloc = false;

if (table.trylock()) {
RefcountMap::iterator it = table.refcnts.find(this);
if (it == table.refcnts.end()) {
//找不到这个值,就销毁对象
do_dealloc = true;
//给这个值设置为,正在销毁标志
table.refcnts[this] = SIDE_TABLE_DEALLOCATING;
} else if (it->second < SIDE_TABLE_DEALLOCATING) {
// SIDE_TABLE_WEAKLY_REFERENCED may be set. Don't change it.
//引用计数已经减到0了,deallocating还没设置
//进行销毁,并且,将销毁标志位置1
do_dealloc = true;
it->second |= SIDE_TABLE_DEALLOCATING;
} else if (! (it->second & SIDE_TABLE_RC_PINNED)) {
//引用计数减一
it->second -= SIDE_TABLE_RC_ONE;
}
table.unlock();
if (do_dealloc && performDealloc) {
((void(*)(objc_object *, SEL))objc_msgSend)(this, SEL_dealloc);
}
return do_dealloc;
}

return sidetable_release_slow(table, performDealloc);
}

dealloc 注释有说明,如果没有额外处理,就直接free,不然先通过object_dispose处理若引用,关联属性等

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
inline void
objc_object::rootDealloc()
{
assert(!UseGC);
if (isTaggedPointer()) return;

//指针型isa && 没被若引用&&没有关联属性&&没有c++创建&&没有用引用计数表
if (isa.indexed &&
!isa.weakly_referenced &&
!isa.has_assoc &&
!isa.has_cxx_dtor &&
!isa.has_sidetable_rc)
{
assert(!sidetable_present());
//直接释放
free(this);
}
else {
object_dispose((id)this);
}
}

dispose会调用destructInstance,这个方法如下,注释有说明

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
void *objc_destructInstance(id obj) 
{
if (obj) {
// Read all of the flags at once for performance.
bool cxx = obj->hasCxxDtor();
bool assoc = !UseGC && obj->hasAssociatedObjects();
bool dealloc = !UseGC;

// This order is important.
//调用c++的销毁方法
if (cxx) object_cxxDestruct(obj);
//移除关联属性
if (assoc) _object_remove_assocations(obj);
//清理引用计数表和weak表
if (dealloc) obj->clearDeallocating();
}

return obj;
}

下面是clearDeallocating,主要是处理引用计数表和弱引用表

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
inline void 
objc_object::clearDeallocating()
{
if (!isa.indexed) {
// Slow path for raw pointer isa.
//清理引用计数表
sidetable_clearDeallocating();
}
else if (isa.weakly_referenced || isa.has_sidetable_rc) {
// Slow path for non-pointer isa with weak refs and/or side table data.
clearDeallocating_slow();
}

assert(!sidetable_present());
}

autoreleasepool

大体思路

autoreleasepool通过AutoreleasePoolPage管理对象,每个线程有一个page,存储在TLS中。page内部以栈的方式组织,大小位4096字节,对应操作系统的内存页。每次添加对象就通过page的压栈存入,存满会创建一个新的page继续存储,page与page通过链表的方式存储。push操作会将一个哨兵(nil)压栈,并返回这个位置的地址。pop会查找这个地址,将栈顶到该位置的对象都release。多次的autoreleasepool,会有多个push,记录多个哨兵的位置,然后pop时pop到对应的位置。

autoreleasepool的实现

我们通常使用自动释放池就是使用@autoreleasepool{},这个block对应一个结构体

1
2
3
4
5
struct __AtAutoreleasePool {
__AtAutoreleasePool() {atautoreleasepoolobj = objc_autoreleasePoolPush();}
~__AtAutoreleasePool() {objc_autoreleasePoolPop(atautoreleasepoolobj);}
void * atautoreleasepoolobj;
};

这个结构题会在初始化的时候调用objc_autoreleasePoolPush,在析构时调用objc_autoreleasePoolPop。 我们在objc源码中找到这两个方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
void *
objc_autoreleasePoolPush(void)
{
if (UseGC) return nil;
return AutoreleasePoolPage::push();
}

void
objc_autoreleasePoolPop(void *ctxt)
{
if (UseGC) return;
AutoreleasePoolPage::pop(ctxt);
}

这两个方法是AutoreleasePoolPage这个类来实现的。
直接从这两个方法看起

1
2
3
4
5
6
7
8
9
10
11
12
13
//添加哨兵POOL_SENTINEL(值为nil),处理page,返回哨兵对象的地址。
static inline void *push()
{
id *dest;
if (DebugPoolAllocation) {
// Each autorelease pool starts on a new pool page.
dest = autoreleaseNewPage(POOL_SENTINEL);
} else {
dest = autoreleaseFast(POOL_SENTINEL);
}
assert(*dest == POOL_SENTINEL);
return dest;
}

下面看下怎么处理的poolpage

1
2
3
4
5
6
7
8
9
10
11
static inline id *autoreleaseFast(id obj)
{
AutoreleasePoolPage *page = hotPage();
if (page && !page->full()) {
return page->add(obj);
} else if (page) {
return autoreleaseFullPage(obj, page);
} else {
return autoreleaseNoPage(obj);
}
}

拿到当前page,如果能拿到并且,page没有存满,就将obj存入
如果page是满的,就走autoreleaseFullPage
如果没拿到page,走autoreleaseNoPage方法

接着看下hotPage()是怎么处理的。

1
2
3
4
5
6
7
static inline AutoreleasePoolPage *hotPage() 
{
AutoreleasePoolPage *result = (AutoreleasePoolPage *)
tls_get_direct(key);
if (result) result->fastcheck();
return result;
}

这个page是放到线程的存储空间的,所以poolpage是线程相关的,一个线程,一个page链。

没有page时,第一次创建成功会将hotpage存起来,会存到线程中。

1
2
3
4
5
static inline void setHotPage(AutoreleasePoolPage *page) 
{
if (page) page->fastcheck();
tls_set_direct(key, (void *)page);
}

至此push就差不多了,总结下push都干了什么

  • 从线程的存储空间中拿到当前页(hotpage),没有的话,就创建一个放进去
  • 查看page有没有满,没满就将传入的哨兵存入。
  • page满了,向链中寻找最后一个节点,创建一个新的page,parent设置为这个节点,将这个节点设置为hotpage。

接下来看看pop

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
static inline void pop(void *token) 
{
AutoreleasePoolPage *page;
id *stop;

page = pageForPointer(token);
stop = (id *)token;
if (DebugPoolAllocation && *stop != POOL_SENTINEL) {
// This check is not valid with DebugPoolAllocation off
// after an autorelease with a pool page but no pool in place.
_objc_fatal("invalid or prematurely-freed autorelease pool %p; ",
token);
}

if (PrintPoolHiwat) printHiwat();

page->releaseUntil(stop);

// memory: delete empty children
if (DebugPoolAllocation && page->empty()) {
// special case: delete everything during page-per-pool debugging
AutoreleasePoolPage *parent = page->parent;
page->kill();
setHotPage(parent);
} else if (DebugMissingPools && page->empty() && !page->parent) {
// special case: delete everything for pop(top)
// when debugging missing autorelease pools
page->kill();
setHotPage(nil);
}
else if (page->child) {
// hysteresis: keep one empty child if page is more than half full
if (page->lessThanHalfFull()) {
page->child->kill();
}
else if (page->child->child) {
page->child->child->kill();
}
}
}

pop操作和push是成对操作,push操作记录的位置,接下来会用来pop。

runtime对autorelease返回值的优化

问题1:为什么要做这个优化?

答:
当返回值被返回之后,紧接着就需要被 retain 的时候,没有必要进行 autorelease + retain,直接什么都不要做就好了。

问题2:如何做的优化?

基本思路:

在返回值身上调用objc_autoreleaseReturnValue方法时,runtime在TLS中做一个标记,然后直接返回这个object(不调用autorelease);同时,在外部接收这个返回值的objc_retainAutoreleasedReturnValue里,发现TLS中有标记,那么直接返回这个object(不调用retain)。
于是乎,调用方和被调方利用TLS做中转,很有默契的免去了对返回值的内存管理。

具体做法:
优化主要是通过两个方法进行实现objc_autoreleaseReturnValue和objc_retainAutoreleasedReturnValue

看第一个方法前,先看个枚举

1
2
3
enum ReturnDisposition : bool {
ReturnAtPlus0 = false, ReturnAtPlus1 = true
};

objc_autoreleaseReturnValue

方法的实现如下,通过注释进行了解释

1
2
3
4
5
6
7
8
// Prepare a value at +1 for return through a +0 autoreleasing convention.
id objc_autoreleaseReturnValue(id obj)
{
//可被优化的场景下,直接返回obj
if (prepareOptimizedReturn(ReturnAtPlus1)) return obj;
//否则还是使用autorelease
return objc_autorelease(obj);
}

对可优化场景的判断,在prepareOptimizedReturn方法中,参数我们根据上边的枚举已经得知
ReturnAtPlus1是true,看下这个方法的实现,用注释做了说明。

1
2
3
4
5
6
7
8
9
10
11
12
static ALWAYS_INLINE bool 
prepareOptimizedReturn(ReturnDisposition disposition)
{
//如果调用方符合优化条件,就返回true,表示这次调用可以被优化
if (callerAcceptsOptimizedReturn(__builtin_return_address(0))) {
//设置dispostion,为后续的objc_retainAutoreleasedReturnValue准备
if (disposition) setReturnDisposition(disposition);
return true;
}
//不符合条件,不做优化
return false;
}

setReturnDisposition是在TLS中存入标记,后续的objc_retainAutoreleasedReturnValue会从TLS中读取来判断,之前是否已经做过了优化。这里比较复杂的方法是callerAcceptsOptimizedReturn判断调用方是否接受一个优化的结果。方法的实现比较难以理解,但是注释说明的比较清楚。

Callee looks for mov rax, rdi followed by a call or
jump instruction to objc_retainAutoreleasedReturnValue or
objc_unsafeClaimAutoreleasedReturnValue.

接收方为上述的两种情况时,调用方就符合优化条件。这个条件其实是判断,是否MRC和ARC混编,如果调用方和被调方一个使用MRC一个使用ARC,就不能做这个优化了。

objc_retainAutoreleasedReturnValue

这个方法相对简单,就是判断之前是否已经做了优化(通过TLS中的RETURN_DISPOSITION_KEY)

1
2
3
4
5
6
7
8
// Accept a value returned through a +0 autoreleasing convention for use at +1.
id objc_retainAutoreleasedReturnValue(id obj)
{ //从TLS中获取RETURN_DISPOSITION_KEY对应的值,为true,就直接返回obj。
//读取完之后,重置为false
if (acceptOptimizedReturn() == ReturnAtPlus1) return obj;
//没有优化,就走正常的retain流程
return objc_retain(obj);
}

这两个方法成对使用,就可以省去将对象添加到autoreleasepool中的操作。

一个对象的内存布局

内存分配&访问

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
struct ListNode {
int val;
struct ListNode *next;
};

struct ListNode *node1 = malloc(sizeof(struct ListNode));
struct ListNode *node2 = malloc(sizeof(struct ListNode));
node1->next = node2;
node1->val = 4;

//长度
printf("length of ListNode *: %lu \n", sizeof(struct ListNode *));
printf("length of int: %lu \n", sizeof(int));
printf("length of ListNode: %lu \n", sizeof(struct ListNode));
//输出
length of ListNode *: 8
length of int: 4
length of ListNode: 16

变量的类型和顺序决定这个对象的内部空间分配。以ListNode为例,第一个区域是val,因为是int类型,占4个字节。第二个区域是next,因为是指针类型,占用8个字节。因为内存对齐的缘故,中间会有一些空白区域。后面对内存对齐进行介绍。

有了这个结构,变量的访问逻辑就比较简单了。访问val的地址与ListNode的实例是一样的,*next是ListNode的地址+8。

内存对齐

一个对象占用的内存,并不是所有变量相加出来的结果。中间会有一些空位补0,来对齐。目的是为了提高内存的访问效率以及平台移植。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
struct StructOne {
char a; //1字节
double b; //8字节
int c; //4字节
short d; //2字节
} MyStruct1;

struct StructTwo {
double b; //8字节
char a; //1字节
short d; //2字节
int c; //4字节
} MyStruct2;
NSLog(@"%lu---%lu--", sizeof(MyStruct1), sizeof(MyStruct2));
//24,16

两个结构体中的组成一样,内存占用却不一样。先看下原则

内存对齐原则:

  • 对于结构体的各个成员,第一个成员的偏移量是0,排列在后面的成员其当前偏移量必须是当前成员类型的整数倍
  • 结构体内所有数据成员各自内存对齐后,结构体本身还要进行一次内存对齐,保证整个结构体占用内存大小是结构体内最大数据成员的最小整数倍
  • 如程序中有#pragma pack(n)预编译指令,则所有成员对齐以n字节为准(即偏移量是n的整数倍),不再考虑当前类型以及最大结构体内类型

在不设置pragma pack的情况下,我们用前两条原则,对上面两个结构体进行分析。
StructOne

  • a字段从0起,占一个字节,offset1
  • b字段不能从2起,按照原则1,要从8开始,占8个字节,offset 16
  • c字段从16起,没有问题,占4个字节,offset 20
  • d字段从20起,没有问题,占2个字节,offset 22
  • 根据原则2,structOne的占位应该是double类型的整数倍,22最近的8的倍数,即24。

应用

Xcode 工具

zombie

为了debug野指针问题(EXC_BAD_ACCESS)。开启zombie模式后,当对象引用计数为0,出发dealloc时,会走特殊的zombie_dealloc。会正常做销毁工作,但是不会释放内存,而是将该isa指针指向一个自定义的zombie_xxx类。在此对这个对象发消息时,会到这个自定义的类。这个类会打印debug。

生成过程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
//1、获取到即将deallocted对象所属类(Class)
Class cls = object_getClass(self);

//2、获取类名
const char *clsName = class_getName(cls)

//3、生成僵尸对象类名
const char *zombieClsName = "_NSZombie_" + clsName;

//4、查看是否存在相同的僵尸对象类名,不存在则创建
Class zombieCls = objc_lookUpClass(zombieClsName);
if (!zombieCls) {
//5、获取僵尸对象类 _NSZombie_
Class baseZombieCls = objc_lookUpClass(“_NSZombie_");

//6、创建 zombieClsName 类
zombieCls = objc_duplicateClass(baseZombieCls, zombieClsName, 0);
}
//7、在对象内存未被释放的情况下销毁对象的成员变量及关联引用。
objc_destructInstance(self);

//8、修改对象的 isa 指针,令其指向特殊的僵尸类
objc_setClass(self, zombieCls);

触发过程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
//1、获取对象class
Class cls = object_getClass(self);

//2、获取对象类名
const char *clsName = class_getName(cls);

//3、检测是否带有前缀_NSZombie_
if (string_has_prefix(clsName, "_NSZombie_")) {
//4、获取被野指针对象类名
const char *originalClsName = substring_from(clsName, 10);

 //5、获取当前调用方法名
 const char *selectorName = sel_getName(_cmd);
  
 //6、输出日志
 Log(''*** - [%s %s]: message sent to deallocated instance %p", originalClsName, selectorName, self);

 //7、结束进程
 abort();

address sanitizer

address sanitizer 比 zombie 多了一些功能,除了检查已经被释放了的对象,还可以检查一些越界问题。

实现

开启 address sanitizer 系统的malloc和free操作会被替换成另一种实现。malloc除了会请求指定大小的内存空间,还会在周围申请一些额外空间,并标记为off-limits。free会将整个区域标记为off-limits。然后添加到quarantine(隔离,检疫)队列。这个队列会使这块区域延后释放。之后内存的访问也会有些变化。

1
2
3
4
5
6
7
8
// 开启前
*address = ...; // or: ... = *address;

// 开启后
if (IsMarkedAsOffLimits(address)) {
ReportError(address);
}
*address = ...; // or: ... = *address;

开启设置后,对off-limits的访问,会报错。

性能影响

开启设置后,CPU性能会下降2到5倍,内存使用增加2到3倍。遇到问题后,开启进行复习,还是可以接受的。

局限

1、只能检查执行的代码,必须复现出来。

2、不能坚持内存泄漏,未初始化的内存,以及Int类型的溢出。

instruments

instruments关于内存的有 allocation 和 leaks。用于检查内存分配和泄漏,用法比较简单,遇到内存涨幅很大的情况,可以用instrument查看,是那些逻辑导致的,以及查看是否是因为内存泄漏导致的内存只增不减。

一些第三方库

因为Xcode提供的工具多是在遇到问题之后才会开启,没办法在开发中实时监测,所以一些开发者写了一些用于在开发期间检查内存问题的库,主要用来检查内存分配和内存泄漏。

MLeaksFinder

假定一个controller被pop出去,或者dismiss掉之后,不久就要被释放。hook navigationController的pop相关方法,以及ViewController的dismiss方法,对即将要释放的Controller开启一个定时器,两秒后触发,如果释放了,就结束了。如果没有释放,两秒后就会对这个控制器进行循环引用检查。

延伸一下,当Controller即将释放时,也会调用属性的类似逻辑,让属性触发自检。循环应用检查,是使用的下面要说的库,FBRetainCycle。

FBAllocationTracker

这个库主要是追踪分配的对象。有两种模式,1、只追踪分配和释放的数量。2、追踪分配的对象。

首先hook对象的分配,即allocWithZone和dealloc方法。

第一种模式的实现很简单,在自己的alloc和dealloc中进行加减计数。

第二种模式会额外的将创建的对象加到字典中,dealloc之后移除。这个库在这里比其他类似的库多做了一点操作,就是分代存储。类似instrument中的mark操作,这个库也有一个mark操作,每次mark,就会创建一个新的字典,用于存储这之后的对象,这个字典会放到一个集合中,表示从开启到目前为止的所以分配对象。

FBRetainCycle

FBRetainCycleDetector 对可疑对象进行深度优先搜索,查找可能的循环引用,并将循环引用链打印出来。

思路比较简单

  • 获取可疑对象的所有strong属性
  • 遍历所有属性,对属性继续进行这两步
  • 直到没有属性,或者,达到最大深度
  • 当有属性是之前遍历过的,说明有循环引用,记录下来

MLeaksFinder找到的只是可疑对象,并不一定是真的泄漏对象。而FBRetainCycle则只能对特定对象进行扫描。两者搭配使用,效果很好。

补充:

NSNumber,NSDate 使用taggedPointer技术,将指针与值保存在一起。看似对象,其实是基础类型。

待了解:
iOS Memory Deep Dive

参考文档:

黑幕背后的Autorelease

冰川

6 posts
2 categories
6 tags
RSS
© 2019 冰川
Powered by Hexo v3.9.0
|
Theme – NexT.Pisces v7.1.2