распределений Tracking легко:
Как использовать: можно поместить 150 строк кода ниже в файл с именем AllocTracker .m и перетащите его в свои файлы проекта. Установите флажок в правой панели Xcode, чтобы включить/отключить его в вашей цели компиляции.
Что вы получите? при включении, этот модуль будет отслеживать все распределения и освобождения от UIImage объектов и зарегистрировать их. (Его можно легко изменить для отслеживания других классов.)
В дополнение к протоколированию каждого распределения и освобождения, он периодически (в настоящее время каждые 15 секунд) сбрасывает все объекты, которые в настоящее время распределены, с некоторой дополнительной информацией и стеком вызовов которые их выделили.
Что такое добавленная стоимость? Этот код использовался в больших проектах, чтобы избавиться от объектов-сирот, которые были оставлены без уведомления, что позволило значительно сократить объем памяти приложения и устранить утечки памяти.
Так вот код для AllocTracker.m:
#define TRACK_ALLOCATIONS
#ifdef TRACK_ALLOCATIONS
#import <UIKit/UIKit.h>
#define TIMER_INTERVAL 15
@implementation UIApplication(utils)
+(NSString *)dateToTimestamp:(NSDate *)date
{
if (date == nil) {
date = [NSDate date];
}
static NSDateFormatter *dateFormatter = nil;
if (!dateFormatter) {
dateFormatter = [[NSDateFormatter alloc] init];
dateFormatter.locale = [[NSLocale alloc] initWithLocaleIdentifier:@"en_US_POSIX"];
[dateFormatter setDateFormat:@"HH:mm:ss.S"];
}
NSString *ts = [dateFormatter stringFromDate:date];
return ts;
}
+(NSString*) getCaller:(int)stackDepth
{
#ifndef DEBUG
return @"NON DBG";
#else
NSArray *symbols = [NSThread callStackSymbols];
int lastIndex = (int)(symbols.count - 1);
if (lastIndex < 3) {
return @"NO DATA";
}
NSMutableString *result = [NSMutableString string];
int foundCount = 0;
for (int ix=3; ix <= lastIndex; ix++) {
NSString *line = symbols[ix];
NSRange rng1 = [line rangeOfString:@"["];
if (rng1.location == NSNotFound) {
continue;
}
NSRange rng2 = [line rangeOfString:@"]"];
NSString *caller = [line substringWithRange:NSMakeRange(rng1.location+1, rng2.location-rng1.location-1)];
if (foundCount > 0) { //not first
[result appendString:@"<--"];
}
[result appendString:caller];
if (++foundCount == stackDepth) {
break;
}
}
return (foundCount > 0) ? result : @"NO SYMBOL";
#endif
}
@end
@implementation UIImage(memoryTrack)
static NSMapTable *g_allocsMap;
static NSTimer *g_tmr;
static NSDate *g_lastDump = nil;
+(void)gotTimer:(NSTimer *)timer
{
[self dumpAllocs];
}
+(void)startTimer
{
static int count = 0;
g_tmr = [NSTimer scheduledTimerWithTimeInterval:15 target:self selector:@selector(gotTimer:) userInfo:@(count++) repeats:YES];
NSLog(@"starting timer %i", count);
}
+(void)cancelTimer
{
[g_tmr invalidate];
g_tmr = nil;
}
+(void)dumpAllocs
{
dispatch_async(dispatch_get_global_queue(QOS_CLASS_BACKGROUND, 0), ^{
NSMutableString *str = [NSMutableString string];
[str appendString:@"\n#$# ========== Non-freed UIImages =========\n"];
NSMutableArray *sorted = [NSMutableArray array];
//make sure map is not changed while enumerating
static int s_ts_start = -1;
@synchronized (g_allocsMap) {
NSEnumerator *keysEnum = [g_allocsMap keyEnumerator];
UIImage *img;
while (img = [keysEnum nextObject]) {
NSString *value = [g_allocsMap objectForKey:img];
if (value) { //might be nulled because declared as weak
NSUInteger memUsed = CGImageGetHeight(img.CGImage) * CGImageGetBytesPerRow(img.CGImage);
NSString *objData = [NSString stringWithFormat:@"mem=%5ikb, size=%4ix%-4i", (int)(memUsed/1024), (int)img.size.width, (int)img.size.height];
NSString *line = [NSString stringWithFormat:@"%p - %@ [%@]\n", img, objData, value];
if (s_ts_start<0) {
s_ts_start = (int)[line rangeOfString:@"["].location + 1;
}
if (line.length > (s_ts_start+10)) {
[sorted addObject:line];
}
}
}
}
if (sorted.count > 0) {
[sorted sortUsingComparator: ^NSComparisonResult(NSString *s1, NSString *s2)
{
//we expect '0x15a973700 - mem=3600kb, size=640x360 [16:14:27.5: UIIma...'
NSString *ts1 = [s1 substringWithRange:NSMakeRange(s_ts_start, 10)];
NSString *ts2 = [s2 substringWithRange:NSMakeRange(s_ts_start, 10)];
return [ts1 compare:ts2];
}];
int ix = 0;
for (NSString *line in sorted) {
[str appendFormat:@"#$# %3i) %@", ix++, line];
}
}
[str appendString:@"#$# ======================================================\n"];
NSLog(@"%@", str);
});
}
+(instancetype)alloc
{
NSString *caller = [UIApplication getCaller:4];
@synchronized (self) {
id obj = [super alloc];
NSLog(@"#$# UIImage alloc: [%p], caller=[%@]", obj, caller);
NSDate *now = [NSDate date];
NSString *value = [NSString stringWithFormat:@"%@: %@", [UIApplication dateToTimestamp:now], caller];
if (!g_allocsMap) {
g_allocsMap = [NSMapTable mapTableWithKeyOptions:NSMapTableWeakMemory valueOptions:NSMapTableStrongMemory];
}
[g_allocsMap setObject:value forKey:obj];
if (!g_lastDump) {
[self startTimer];
g_lastDump = now;
}
return obj;
}
}
-(void)dealloc
{
NSLog(@"#$# UIImage dealloc: [%@]", self);
}
@end
#endif //TRACK_ALLOCATIONS
Как это работает? Мы создаем категорию UIImage и устанавливаем нашу собственную версию для alloc и dealloc. Каждый выделенный объект сохраняется в объекте NSMapTable, который работает как словарь, но позволяет хранить объект со слабыми указателями.
Для удобства мы добавили два метода в UIApplication, которые могут использоваться другими модулями, если создан соответствующий файл заголовка. Один из способов - форматирование метки времени, а другой - для чтения стека вызовов (работает только в отладочных сборках).
Подсказка для использования: если вы используете реальное устройство и установить idevicesyslog (brew install libimobiledevice
), вы можете использовать терминал, чтобы увидеть все выделения отладки, например:
idevicesyslog | grep "#\$#"