- PVSM.RU - https://www.pvsm.ru -
Пришла мне как-то в голову идея, а можно ли взять блок и отдать для target-action?
Есть готовые решения, как к примеру BlocksKit [1] и другие библиотеки, однако их решение заключается в сохранении блока, установкой таргета и вызова блока из указанного селектора.
Зачем тогда нужна эта статья?
Я захотел создать способ генерации селектора, по которому будет вызван блок. Что здесь сложного, скажете вы? imp_implementationWithBlock + class_addMethod и дело закрыто. Но при этом подходе есть одно серьезное требование, это первый аргумент блока — владелец метода.
Как обойти это требование и сделать такое?
[button addTarget:self action:[self ax_lambda:^(UIButton *sender, UIEvent *event){
NSLog(@"click on button %@, event = %@", sender, event);
}] forControlEvents:UIControlEventTouchUpInside];
[button addTarget:self action:[self ax_lambda:^{
NSLog(@"click");
}] forControlEvents:UIControlEventTouchUpInside];
Или даже вот так
__block NSInteger sum = 0;
[self performSelector:[self ax_lambda:^(NSNumber *argA, NSNumber *argB) {
sum = [argA integerValue] + [argB integerValue];
}] withObject:@(2) withObject:@(3)];
//sum — 5
SEL selSum = [self ax_lambda:^NSInteger(NSInteger argA, NSInteger argB){
return argA + argB;
}];
NSInteger(*funcSum)(id, SEL, NSInteger, NSInteger) = (NSInteger(*)(id, SEL, NSInteger, NSInteger))objc_msgSend;
NSInteger sum2 = funcSum(self, selSum, 2, 3);
//sum2 — 5
Реализация оказалась настолько интересной, что я решил написать об этом.
По сути, основная задача это избавиться от первого аргумента self в вызове блока. Это корневая проблема всего решения (жаль, что не единственная).
Ранее я уже немного писал о блоках [2], и отметил, что блок — это объект, а значит вызов будет происходить через NSInvocation.
Если получить момент вызова блока и в NSInvocation убрать аргумент self (сдвинув аргументы), то тогда я получу желаемый результат.
Дальше надо будет разбираться по ходу дела.
Вопрос, как вклиниться в момент вызова блока? Как вообще получить момент вызова блока?
Очень часто я пишу эту фразу, но блок — это объект. Объект в objc в конечном виде это структура. Раз id — это указатель на структуру, позволено и обратное (__bridge, привет) [3].
Получается, можно создать фейковый блок. Ну или прокси для блока.
typedef void(^AXProxyBlockInterpose)(NSInvocation *invocation);
@interface AXProxyBlock : NSProxy
+ (instancetype)initWithBlock:(id)block;
- (void)setBeforeInvoke:(AXProxyBlockInterpose)beforeInvoke;
- (NSString *)blockSignatureStringCTypes;
@end
Как можно догадаться, setBeforeInvoke принимает блок, в котором можно делать «магические» преобразования аргументов блока.
blockSignatureStringCTypes возвращает сигнатуру проксируемого блока. Зачем он в заголовочном файле? Об этом позднее.
ссылка на страницу документации о том, что сейчас начнется [4]
typedef struct AXBlockStruct_1 {
unsigned long int reserved;
unsigned long int size;
void (*copy_helper)(void *dst, void *src);
void (*dispose_helper)(void *src);
const char *signature;
} AXBlockStruct_1;
typedef struct AXBlockStruct {
void *isa;
int flags;
int reserved;
void (*invoke)(void *, ...);
struct AXBlockStruct_1 *descriptor;
} AXBlockStruct;
typedef NS_ENUM(NSUInteger, AXBlockFlag) {
AXBlockFlag_HasCopyDispose = (1 << 25),
AXBlockFlag_HasCtor = (1 << 26),
AXBlockFlag_IsGlobal = (1 << 28),
AXBlockFlag_HasStret = (1 << 29),
AXBlockFlag_HasSignature = (1 << 30)
};
А теперь займемся нашим классом.
@interface AXProxyBlock () {
// isa поле уже имеется в реализации NSProxy, а остальные поля добавим
int _flags;
int _reserved;
IMP _invoke;
AXBlockStruct_1 *_descriptor;
// готово, а теперь те поля, которые нужны для класса
AXProxyBlockInterpose _beforeInvoke;
id _block;
NSMethodSignature *_blockMethodSignature;
IMP _impBlockInvoke;
}
@end
Теперь нужно чтобы на момент вызова класс имитировал принимаемый блок:
- (instancetype)initWithBlock:(id)block {
if (self != nil) {
AXBlockStruct *blockRef = (__bridge AXBlockStruct *)block;
_flags = blockRef->flags;
_reserved = blockRef->reserved;
_descriptor = calloc(1, sizeof(AXBlockStruct_1));
_descriptor->size = class_getInstanceSize([self class]);
BOOL flag_stret = _flags & AXBlockFlag_HasStret;
_invoke = (flag_stret ? (IMP)_objc_msgForward_stret : (IMP)_objc_msgForward);
...
Описание назначения этих полей можно прочитать все на той же странице документации clang [4]. Теперь поля соответствуют блоку на момент вызова.
Но у меня есть 2 очень важных ivar, которые я не стал включать под спойлер выше, поскольку они относятся уже к вызову блока и на них я хочу остановится более подробно.
_impBlockInvoke = (IMP)blockRef->invoke;
_blockMethodSignature = [self blockMethodSignature];
_impBlockInvoke — это функция вызова блока, имплементация. Это обычный указатель на функцию и вызвать можно руками.
_blockMethodSignature это метод-сигнатура блока. Что это такое будет рассмотрено очень подробно далее.
- (NSMethodSignature *)blockMethodSignature {
const char *signature = [[self blockSignatureStringCTypes] UTF8String];
return [NSMethodSignature signatureWithObjCTypes:signature];
}
- (NSString *)blockSignatureStringCTypes {
AXBlockStruct *blockRef = (__bridge AXBlockStruct *)_block;
const int flags = blockRef->flags;
void *signatureLocation = blockRef->descriptor;
signatureLocation += sizeof(unsigned long int);
signatureLocation += sizeof(unsigned long int);
if (flags & AXBlockFlag_HasCopyDispose) {
signatureLocation += sizeof(void(*)(void *dst, void *src));
signatureLocation += sizeof(void (*)(void *src));
}
const char *signature = (*(const char **)signatureLocation);
return [NSString stringWithUTF8String:signature];
}
Мы берем наш блок, получаем из него descriptor, потом смещаемся на нужную величину для получения сигнатуры блока (const char *) и через нее создаем NSMethodSignature. NSMethodSignature определяет кол-во и типы аргументов, возвращаемое значение и тп.
Выглядит не сложно, но манипуляции с флагом смущают: в зависимости от типа блока, его сигнатура может располагаться по-разному. К примеру, у глобального блока не нужно смещаться за функции копирования и разрушения.
Метода на вызов блока у моего класса нет, значит вызван будет forwardInvocation, а перед ним необходимо узнать какого типа будет сформирован NSInvocation, поэтому происходит вызов methodSignatureForSelector, в котором мы отдаем наш _blockMethodSignature.
- (void)forwardInvocation:(NSInvocation *)anInvocation {
[anInvocation setTarget:_block];
if (_beforeInvoke) {
_beforeInvoke(anInvocation);
}
IMP imp = _impBlockInvoke;
[anInvocation invokeUsingIMP:imp];
}
Код здесь должен быть очень понятен (установили новую цель на вызов, вызвали блок before если существует), но где вызов [anInvocation invoke]?!
Это черная магия. Метод invokeUsingIMP это private API, которое можно найти здесь, как и еще много чего [5]
Я думаю, что проксирование блока материал своеобразный и если перейти сразу к решению второй половины задачи, то статью дочитает меньше людей. Поэтому сейчас будет мельком рассматриваться обертка как собирание пазла готовых решений и в конце будет разбираться вторая половина задачи. Это позволит немного расслабиться и собрать материал более структурировано.
Поговорим о методе, который вызывался в самом начале статьи — ax_lambda. Это всего лишь категория для NSObject, она является оберткой для вызова основной функции, которая выглядит следующим образом:
SEL ax_lambda(id obj, id block, NSMutableArray *lambdas);
Думаю теперь становится понятнее, для чего написана обертка. И если первый и второй аргумент не вызывает вопросов, то 3й заставляет задуматься. Сперва я расскажу о необходимости третьего аргумента, а потом уже приведу под спойлеры код категории.
SEL ax_lambda(id obj, id block, NSMutableArray *lambdas) {
SEL selector = ax_generateFreeSelector(obj);
AXProxyBlockWithSelf *proxyBlock = [AXProxyBlockWithSelf initWithBlock:block];
[proxyBlock setBeforeInvoke:^(NSInvocation *invocation){
ax_offsetArgInInvocation(invocation);
}];
[lambdas addObject:proxyBlock];
IMP imp = imp_implementationWithBlock(proxyBlock);
NSString *signatureString = [proxyBlock blockSignatureStringCTypes];
class_addMethod([obj class], selector, imp, [signatureString UTF8String]);
return selector;
}
Это и есть основная функция, тот самый собранный пазл. Класс AXProxyBlockWithSelf будет рассмотрен далее, пока только отмечу, что это потомок класса AXProxyBlock как наверняка догадались.
Чтобы сделать блок методом необходим селектор, имплементация и строковая сигнатура. Имплементация будет получена с проксиблока, строковую сигнатуру отдаст тоже прокси (в AXProxyBlock это сигнатура проксируемого блока, но в AXProxyBlockWithSelf она отличается и это будет рассмотрено далее), ну а селектор сгенерировать не сложно. Так зачем же 3й параметр?
При вызове imp_implementationWithBlock будет вызвано копирование блока (Block_copy). Поле copy_helper в блоке указатель на функцию копирования блока. Однако прокси блока не имеет такой возможности. Даже если я создам функцию копирования вида void (*)(void *dst, void *src), я не смогу получить желаемый результат. В src придет объект, в который нужно копировать и это будет не экземпляр моего класса. Поэтому вызов imp_implementationWithBlock не увеличит счетчик ссылок для объекта proxyBlock (и proxyBlock будет уничтожен после завершения функции). Чтобы этого не допустить, я использую коллекцию, которая увеличит внутренний счетчик ссылок. Получается срок жизни блока зависит от срока жизни коллекции хранящей его. В случае с категорией срок жизни блока ограничен сроком жизни владельца.
SEL ax_lambda(id obj, id block, NSMutableArray *lambdas);
@interface NSObject (AX_Lambda)
- (SEL)ax_lambda:(id)block;
@end
static char kAX_NSObjectAssociatedObjectKey;
@interface NSObject (_AX_Lambda)
@property (copy, nonatomic) NSMutableArray *ax_lambdas;
@end
@implementation NSObject (_AX_Lambda)
@dynamic ax_lambdas;
- (void)setAx_lambdas:(NSMutableArray *)lambdas {
objc_setAssociatedObject(self, &kAX_NSObjectAssociatedObjectKey, lambdas, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
- (NSMutableArray *)ax_lambdas {
NSMutableArray *marrey = objc_getAssociatedObject(self, &kAX_NSObjectAssociatedObjectKey);
if (marrey == nil) {
self.ax_lambdas = [NSMutableArray array];
}
return objc_getAssociatedObject(self, &kAX_NSObjectAssociatedObjectKey);
}
@end
@implementation NSObject (AX_Lambda)
- (SEL)ax_lambda:(id)block {
return ax_lambda(self, block, self.ax_lambdas);
}
@end
Ну и функции, используемые в SEL ax_lambda(id obj, id block, NSMutableArray *lambdas);
SEL ax_generateFreeSelector(id obj) {
SEL selector;
NSMutableString *mstring = [NSMutableString string];
do {
[mstring setString:@"ax_rundom_selector"];
u_int32_t rand = arc4random_uniform(UINT32_MAX);
[mstring appendFormat:@"%zd", rand];
selector = NSSelectorFromString(mstring);
} while ([obj respondsToSelector:selector]);
return selector;
}
void ax_offsetArgInInvocation(NSInvocation *invocation) {
void *foo = malloc(sizeof(void*));
NSInteger arguments = [[invocation methodSignature] numberOfArguments];
for (NSInteger i = 1; i < arguments-1; i++) { //i = 0 is self
[invocation getArgument:foo atIndex:i+1];
[invocation setArgument:foo atIndex:i];
}
free(foo);
}
Перед тем, как приступить к следующей части, необходимо базовое понимание работы NSInvocation и NSMethodSignature. Я думал выделить это в отдельную статью, но пришел к выводу, что если очень не углубляться в материал, то статья получится пусть интересной и простой (в разборе конкретного примера), но очень не большой. Поэтому я решил написать об этом прямо здесь.
Мне нужен был метод, позволяющий генерировать строку из формата и массива аргументов, к примеру вот так:
NSString *format = @"%@, foo:%@, hello%@";
NSArray *input = @[@(12), @(13), @" world"];
NSString *result = [NSString ax_stringWithFormat:format array:input];
//result — @"12, foo:13, hello world"
К сожалению, методы, которые я находил на SO не работали (первый [6], второй [7]). Возможно я не правильно пытался их использовать (у кого получилось — отпишите, пожалуйста) на ARC, но поскольку мне нужен был рабочий вариант, я написал свою реализацию.
Не делая никаких обходов с указателями или преобразованиями, решение основывается полностью на принципе работы методов.
Конечный вид метода выглядит так:
+ (instancetype)ax_stringWithFormat:(NSString *)format array:(NSArray *)arguments;
Стандартный метод для создания строки по формату и параметрам выглядит следующим образом
- (instancetype)initWithFormat:(NSString *)format arguments:(va_list)argList NS_FORMAT_FUNCTION(1,0);
Но для использования (и самая проблема) нужно создать va_list (что это [8] и как использовать [9]).
Следующий метод отлично подходит
+ (instancetype)ax_string:(NSString *)format, ... {
va_list list;
va_start(list, format);
NSString *str = [[NSString alloc] initWithFormat:format arguments:list];
va_end(list);
return str;
}
Теперь проблема как его вызвать с аргументами из NSArray.
NSInvocation [10] — это объект используемый для хранения и пересылки сообщения между объектами и/или между приложениями.
Однако при создании NSInvocation нужно иметь NSMethodSignature.
NSMethodSignature [11] позволяет определить сколько аргументов принимает метод, типы аргументов, смещения, тип возвращаемого значения. По этом очень логичным смотрится замечание из документации [10]
NSInvocation does not support invocations of methods with either variable numbers of arguments or union arguments.
Ведь не известно сколько аргументов и какого типа будет передано в функцию/метод с переменным кол-вом аргументов.
А если все же известно? Если я сам знаю эту информацию перед вызовом? Тогда я могу сказать, что в данном случае метод будет принимать к примеру 4 аргумента и тк функция принимает переменное кол-во аргументов, это сработает.
NSMethodSignature можно создать через генерируемую сигнатуру, если самому указать всю информацию выше. NSArray содержит только указатели и смещения всех параметров только на величину указателя, поэтому все довольно просто. Как я уже писал [12], в методе можно использовать self и _cmd потому что они в неявном виде передаются в метод.
+ (NSMethodSignature *)ax_generateSignatureForArguments:(NSArray *)arguments {
NSInteger count = [arguments count];
NSInteger sizeptr = sizeof(void *);
NSInteger sumArgInvoke = count + 3; // self + _cmd + не забыть про то что в метод еще и формат будет передаваться
NSInteger offsetReturnType = sumArgInvoke * sizeptr;
NSMutableString *mstring = [[NSMutableString alloc] init];
[mstring appendFormat:@"@%zd@0:%zd", offsetReturnType, sizeptr];
for (NSInteger i = 2; i < sumArgInvoke; i++) {
[mstring appendFormat:@"@%zd", sizeptr * i];
}
return [NSMethodSignature signatureWithObjCTypes:[mstring UTF8String]];
}
Стоит немного рассказать о том, что здесь происходит. Для начала надо посмотреть здесь [13] типы кодирования.
А теперь по порядку, я очень надеюсь, что вы посмотрели в таблицу.
На первом месте сигнатуры будет возвращаемый тип и его смещение (возвращаемый тип находится после всех аргументов, поэтому у него будет максимальное смещение, но пишется на первом). Предположим sizeof(void*) будет 8 и массив из 3х аргументов. Но включая self + _cmd + формат который будет передан и того получаем 6 аргументов. 6х8 = 48
@48
Затем следует self и _cmd. self на первом месте в аргументах, по этому
@48@0:8
Затем формат
@48@0:8@16
и аргументы
@48@0:8@16@24@32@40
Теперь, имея сигнатуру можно использовать NSInvocation
+ (instancetype)ax_stringWithFormat:(NSString *)format array:(NSArray *)arrayArguments {
NSMethodSignature *methodSignature = [self ax_generateSignatureForArguments:arrayArguments];
NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:methodSignature];
[invocation setTarget:self];
[invocation setSelector:@selector(ax_string:)];
[invocation setArgument:&format atIndex:2];
for (NSInteger i = 0; i < [arrayArguments count]; i++) {
id obj = arrayArguments[i];
[invocation setArgument:(&obj) atIndex:i+3];
}
[invocation invoke];
__autoreleasing NSString *string;
[invocation getReturnValue:&string];
return string;
}
И теперь, если немного изменить метод выше, можно избавиться от метода + (instancetype)ax_string:(NSString *)format,…
+ (instancetype)ax_stringWithFormat:(NSString *)format array:(NSArray *)arrayArguments {
NSMethodSignature *methodSignature = [self ax_generateSignatureForArguments:arrayArguments];
NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:methodSignature];
[invocation setTarget:self];
[invocation setSelector:@selector(stringWithFormat:)];
[invocation setArgument:&format atIndex:2];
for (NSInteger i = 0; i < [arrayArguments count]; i++) {
id obj = arrayArguments[i];
[invocation setArgument:(&obj) atIndex:i+3];
}
[invocation invoke];
__autoreleasing NSString *string;
[invocation getReturnValue:&string];
return string;
}
//https://developer.apple.com/library/mac/documentation/Cocoa/Conceptual/ObjCRuntimeGuide/Articles/ocrtTypeEncodings.html
+ (NSMethodSignature *)ax_generateSignatureForArguments:(NSArray *)arguments {
NSInteger count = [arguments count];
NSInteger sizeptr = sizeof(void *);
NSInteger sumArgInvoke = count + 3; //self + _cmd + (NSString *)format
NSInteger offsetReturnType = sumArgInvoke * sizeptr;
NSMutableString *mstring = [[NSMutableString alloc] init];
[mstring appendFormat:@"@%zd@0:%zd", offsetReturnType, sizeptr];
for (NSInteger i = 2; i < sumArgInvoke; i++) {
[mstring appendFormat:@"@%zd", sizeptr * i];
}
return [NSMethodSignature signatureWithObjCTypes:[mstring UTF8String]];
}
Был рассмотрен перехват момента вызова блока и смещение аргументов. Был рассмотрен код применения идеи и небольшие нюансы этого применения. Однако осталась проблема, которая мешает завершению.
Блок, принимаемый в imp_implementationWithBlock, должен принимать первым аргументов владельца. Получается, что сигнатура входного блока для функции ax_lambda отличается от положенной сигнатуры и в NSInvocation аргументы будут переданы совершенно не верно.
Класс AXProxyBlockWithSelf переделывает сигнатуру проксируемого блока, добавляя в нее дополнительно первый аргумент. Таким образом, вызов проксиблока будет совершен с правильными аргументами, а первый аргумент уже сместим перед вызовом самого блока.
Нужно переписать метод — (NSString *)blockSignatureStringCTypes
- (NSString *)blockSignatureStringCTypes {
NSString *signature = [super blockSignatureStringCTypes];
NSString *unformatObject = [signature ax_unformatDec];
NSString *formatNewSignature = [self addSelfToFormat:unformatObject];
NSArray *byteSignature = [signature ax_numbers];
NSArray *byteNewSignature = [self changeByteSignature:byteSignature];
return [NSString ax_stringWithFormat:formatNewSignature array:byteNewSignature];
}
Итак, имеется сигнатура блока, с типами аргументов и смещением, возвращаемый тип и тп
Нужно вставить дополнительный аргумент в сигнатуру и сместить аргументы.
- (NSString *)ax_unformatDec {
NSCharacterSet *characterSet = [NSCharacterSet decimalDigitCharacterSet];
NSPredicate *predicate = [NSPredicate predicateWithFormat:@"length > 0"];
NSArray *separated = [[self componentsSeparatedByCharactersInSet:characterSet] filteredArrayUsingPredicate:predicate];
NSString *format = [separated componentsJoinedByString:@"%@"];
if ([[self lastSubstring] isEqualToString:[format lastSubstring]] ) {
return format;
} else {
return [format stringByAppendingString:@"%@"];
}
}
- (NSString *)lastSubstring {
NSInteger lastIndex = [self length] - 1;
return [self substringFromIndex:lastIndex];
}
Далее надо посмотреть здесь [13] типы кодирования.
- (NSString *)addSelfToFormat:(NSString *)format {
NSMutableArray *marray = [[format componentsSeparatedByString:@"?"] mutableCopy];
[marray insertObject:@"?%@@" atIndex:1];
return [marray componentsJoinedByString:@""];
}
- (NSArray *)ax_numbers {
NSString *pattern = @"\d+";
NSRegularExpression *regex = [NSRegularExpression regularExpressionWithPattern:pattern options:NSRegularExpressionCaseInsensitive error:nil];
NSRange fullRange = NSMakeRange(0, [self length]);
NSArray *matches = [regex matchesInString:self options:NSMatchingReportProgress range:fullRange];
NSMutableArray *numbers = [NSMutableArray array];
for (NSTextCheckingResult *checkingResult in matches) {
NSRange range = [checkingResult range];
NSString *numberStr = [self substringWithRange:range];
NSNumber *number = @([numberStr integerValue]);
[numbers addObject:number];
}
return numbers;
}
- (NSArray *)changeByteSignature:(NSArray *)byteSignature {
NSInteger value = sizeof(void *);
NSMutableArray *marray = [NSMutableArray array];
for (NSNumber *number in byteSignature) {
NSInteger offset = [number integerValue] + value;
[marray addObject:@(offset)];
}
[marray insertObject:@0 atIndex:1];
return marray;
}
Ну и в конце создаем новую сигнатуру, используя новую формат-строку и NSArray с новым смещением. Таким образом, при вызове имплементации будет передан согласно документации владелец как первый аргумент, смещен благодаря перехвату и вызван оригинальный блок.
Полный код здесь. [14] Это был всего лишь эксперимент, у меня не было желания написать этот код для использования в проектах. Но я рад, что смог завершить это дело успешно. Так же я рад, что возможно смог кому-то помочь выложив решение [15] генерации строки с использованием NSArray на SO.
Надеюсь, у меня получилось донести материал в понятной форме и разбить на блоки.
Автор: ajjnix
Источник [16]
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/ios-development/112820
Ссылки в тексте:
[1] BlocksKit: https://github.com/zwaldowski/BlocksKit
[2] о блоках: https://habrahabr.ru/post/271255/
[3] (__bridge, привет): http://clang.llvm.org/docs/AutomaticReferenceCounting.html#bridged-casts
[4] ссылка на страницу документации о том, что сейчас начнется: http://clang.llvm.org/docs/Block-ABI-Apple.html
[5] private API, которое можно найти здесь, как и еще много чего: https://github.com/nst/iOS-Runtime-Headers
[6] первый: http://stackoverflow.com/questions/1058736/how-to-create-a-nsstring-from-a-format-string-like-xxx-yyy-and-a-nsarr
[7] второй: http://stackoverflow.com/questions/8273380/converting-nsarray-contents-to-a-varargs-with-arc-for-use-with-nsstring-initwi
[8] что это: http://www.cplusplus.com/reference/cstdarg/va_list/
[9] как использовать: http://www.cplusplus.com/reference/cstdarg/va_start/
[10] NSInvocation: https://developer.apple.com/library/mac/documentation/Cocoa/Reference/Foundation/Classes/NSInvocation_Class/
[11] NSMethodSignature: https://developer.apple.com/library/mac/documentation/Cocoa/Reference/Foundation/Classes/NSMethodSignature_Class/index.html#//apple_ref/doc/c_ref/NSMethodSignature
[12] писал: https://habrahabr.ru/post/270913/
[13] здесь: https://developer.apple.com/library/mac/documentation/Cocoa/Conceptual/ObjCRuntimeGuide/Articles/ocrtTypeEncodings.html
[14] Полный код здесь.: https://github.com/ajjnix/AXBlock
[15] решение: http://stackoverflow.com/a/35039384/4759124
[16] Источник: https://habrahabr.ru/post/276599/
Нажмите здесь для печати.