Краткое описание
В процессе разработки приложений для iPhone очень часто возникают проблемы с утечками памяти, увы стандартные инструменты не сильно помогают в их устранении, поэтому я написал эту библиотеку. С ее помощью можно узнать сколько объектов в текущий момент находится в памяти, какого они класса и в где были созданы.
Исходники
https://github.com/soniccat/memCheck-for-iOS
Подключение
добавить все 6 файлов к проекту
NSMemCheckObject.h
NSMemCheckObject.m
NSMutableArray+MemCheck.h
NSMutableArray+MemCheck.m
NSObject+MemCheck.h
NSObject+MemCheck.m
добавить в метод application: didFinishLaunchingWithOptions:
#ifdef MEMTEST_ON
[NSObject turnMemCheckOn];
#endif
вниз вашего .pch
#define MEMTEST_ON
Использование
В момент когда нужно отобразить занимаемую память нажать на паузу в Debugger-е и ввести в gdb
po [memData allMem]
будет ответ вида
14 items
(
"2011-02-23 06:47:04 +0000 memCheckObject 0x4b45840 object 0x4b45530 stack 0x4b45a00 NSCountedSet",
"2011-02-23 06:47:04 +0000 memCheckObject 0x4e16040 object 0x4e15d00 stack 0x4e15c40 NSAutoreleasePool",
"2011-02-23 06:47:04 +0000 memCheckObject 0x4e10cc0 object 0x4b0de50 stack 0x4e14ef0 __NSPlaceholderDictionary",
"2011-02-23 06:47:04 +0000 memCheckObject 0x4b44ab0 object 0x4b44a90 stack 0x4b42d10 CALayerArray",
"2011-02-23 06:47:04 +0000 memCheckObject 0x4e16cd0 object 0x4e02b80 stack 0x4e16f40 __NSPlaceholderArray",
"2011-02-23 06:47:04 +0000 memCheckObject 0x4e151d0 object 0x4e14ec0 stack 0x4e16380 NSKeyValueMethodSetter",
"2011-02-23 06:47:04 +0000 memCheckObject 0x4e15040 object 0x4e15190 stack 0x4e16060 CALayer",
"2011-02-23 06:47:04 +0000 memCheckObject 0x4e0faf0 object 0x4e08960 stack 0x4e15d20 __NSPlaceholderSet",
"2011-02-23 06:47:04 +0000 memCheckObject 0x4e14de0 object 0x4b10790 stack 0x4e14f60 NSPlaceholderString",
"2011-02-23 06:47:04 +0000 memCheckObject 0x4e06c60 object 0x4b3fdc0 stack 0x4e106f0 NSAutoreleasePool",
"2011-02-23 06:47:04 +0000 memCheckObject 0x4b41cd0 object 0x4b41750 stack 0x4b43450 __NSPlaceholderDate",
"2011-02-23 06:47:04 +0000 memCheckObject 0x4b44100 object 0x4b440f0 stack 0x4b440a0 __NSPlaceholderTimeZone",
"2011-02-23 06:47:04 +0000 memCheckObject 0x4b43680 object 0x4e02b70 stack 0x4b438f0 __NSPlaceholderArray",
"2011-02-23 06:47:04 +0000 memCheckObject 0x4b41960 object 0x4b02420 stack 0x4b43750 NSObject"
)
где 14 это кол-во объектов создание которых было зарегистрировано (вверху самый новый), после memCheckObject идет адрес на объект который сохраняет данные о выделенных объектах, после object идет адрус объекта, после stack адрес на получение стека до места где объект был создан и в конце имя класса самого объекта
po [memData top:10]
возвращает список из последних 10 выделенных объектов, число можно менять на какое вздумается
po 0x4b02420
вернет описание объекта
po 0x4b43750
Вернет стек до места создания:
<_nscallstackarray>(
0 CoreFoundation 0x00da9be9 __exceptionPreprocess + 185,
1 libobjc.A.dylib 0x00efe5c2 objc_exception_throw + 47,
2 inFoundation 0x00002796 +[NSObject(memCheck) myAllocFunc] + 918,
3 inFoundation 0x00001c81 -[inFoundationAppDelegate application:didFinishLaunchingWithOptions:] + 161,
4 UIKit 0x002b31fa -[UIApplication _callInitializationDelegatesForURL:payload:suspended:] + 1163,
...
)
значимая строчка для нас это 4ая, именно в application: didFinishLaunchingWithOptions объект и был создан
Самый простой алгоритм поиска утечек памяти следующий. Запоминаем количество выделенных объектов, заходим в контроллер, жмем back, смотрим сколько объектов теперь, если их стало больше то уже внимательнее смотрим на callstack n верхних элементов, где n это количество новых объектов.
po [0x4b41960 history]
возвращает callstack-и до вызовов alloc, retain и release начиная с самого старого
ALLOC:
2011-02-23 06:26:56 +0000
(
0 CoreFoundation 0x00dacbe9 __exceptionPreprocess + 185
1 libobjc.A.dylib 0x00f015c2 objc_exception_throw + 47
2 memCheck 0x0000310f +[NSObject(memCheck) myAllocFunc] + 831
3 memCheck 0x000024d2 -[inFoundationAppDelegate application:didFinishLaunchingWithOptions:] + 98
4 UIKit 0x002b61fa -[UIApplication _callInitializationDelegatesForURL:payload:suspended:] + 1163
...
)
RETAIN:
2011-02-23 06:26:56 +0000
(
0 CoreFoundation 0x00dacbe9 __exceptionPreprocess + 185
1 libobjc.A.dylib 0x00f015c2 objc_exception_throw + 47
2 memCheck 0x000034cf -[NSObject(memCheck) myRetainFunc] + 335
3 CoreFoundation 0x00cbf0bc CFRetain + 92
4 CoreFoundation 0x00da5db5 +[__NSArrayI __new::] + 117
5 CoreFoundation 0x00d188a3 +[NSArray arrayWithObject:] + 67
6 memCheck 0x00002508 -[inFoundationAppDelegate application:didFinishLaunchingWithOptions:] + 152
7 UIKit 0x002b61fa -[UIApplication _callInitializationDelegatesForURL:payload:suspended:] + 1163
...
)
RETAIN:
2011-02-23 06:26:56 +0000
(
0 CoreFoundation 0x00dacbe9 __exceptionPreprocess + 185
1 libobjc.A.dylib 0x00f015c2 objc_exception_throw + 47
2 memCheck 0x000034cf -[NSObject(memCheck) myRetainFunc] + 335
3 memCheck 0x00002538 -[inFoundationAppDelegate application:didFinishLaunchingWithOptions:] + 200
4 UIKit 0x002b61fa -[UIApplication _callInitializationDelegatesForURL:payload:suspended:] + 1163
...
)
RELEASE:
2011-02-23 06:26:56 +0000
(
0 CoreFoundation 0x00dacbe9 __exceptionPreprocess + 185
1 libobjc.A.dylib 0x00f015c2 objc_exception_throw + 47
2 memCheck 0x000037ee -[NSObject(memCheck) myReleaseFunc] + 302
3 memCheck 0x0000258d -[inFoundationAppDelegate application:didFinishLaunchingWithOptions:] + 285
4 UIKit 0x002b61fa -[UIApplication _callInitializationDelegatesForURL:payload:suspended:] + 1163
...
)
RETAIN:
2011-02-23 06:26:56 +0000
(
0 CoreFoundation 0x00dacbe9 __exceptionPreprocess + 185
1 libobjc.A.dylib 0x00f015c2 objc_exception_throw + 47
2 memCheck 0x000034cf -[NSObject(memCheck) myRetainFunc] + 335
3 CoreFoundation 0x00cbf0bc CFRetain + 92
4 CoreFoundation 0x00da5db5 +[__NSArrayI __new::] + 117
5 CoreFoundation 0x00d188a3 +[NSArray arrayWithObject:] + 67
6 memCheck 0x000025ae -[inFoundationAppDelegate application:didFinishLaunchingWithOptions:] + 318
7 UIKit 0x002b61fa -[UIApplication _callInitializationDelegatesForURL:payload:suspended:] + 1163
...
)
po [0x4b41960 retains] и po [0x4b41960 releases]
возвращают callstack-и для вызовов retain и release соответственно, начиная с самого раннего
po [memData markHeap]
сохраняет текущую дату разбивая тем самым список объектов на созданных до и после
po [memData showHeaps]
выводит список групп из выделенных объектов разбитый с помощью markHeap
po [memData objectsForHeap:n]
выводит список созданных объектов для указанной группы
Ограничения
работают только с iOS 4.0 и выше ( из за доступа к стеку )
Описание устройства
По прочтению умных статей c NSBlog :
http://www.mikeash.com/pyblog/friday-qa-2009-01-23.html
http://www.mikeash.com/pyblog/friday-qa-2009-03-13-intro-to-the-objective-c-runtime.html
http://www.mikeash.com/pyblog/friday-qa-2009-03-27-objective-c-message-forwarding.html
я понял как работает obj_msgSend, и что IMP это обычный указатель на функцию который можно подменить у любого метода NS объекта. В итоге сохранив старый IMP мы можем в вызове собственного метода вызвать базовый, что нельзя было сделать с помощью категорий.
При инициализации я подменяю базовые методы своими с помощью method_exchangeImplementations:
typedef id (*OverrideMemCheckPrototipe)(id,SEL);
#define ALLOC_METHOD_EXCHANGE method_exchangeImplementations(classAllocMethod, classMyAllocMethod)
#define RETAIN_METHOD_EXCHANGE method_exchangeImplementations(classRetainMethod, classMyRetainMethod)
+ (void)turnMemCheckOn
{
if( memData == nil )
memData = [[NSMutableArray allocWithZone:nil] init];
//alloc
classAllocMethod = class_getClassMethod([NSObject class], @selector(alloc) );
classAllocImp = method_getImplementation(classAllocMethod);
classMyAllocMethod = class_getClassMethod([NSObject class], @selector(myAllocFunc) );
classMyAllocImp = method_getImplementation(classMyAllocMethod);
//dealloc
classDeallocMethod = class_getInstanceMethod([NSObject class], @selector(dealloc) );
classDeallocImp = method_getImplementation(classDeallocMethod);
classMyDeallocMethod = class_getInstanceMethod([NSObject class], @selector(myDeallocFunc) );
classMyDeallocImp = method_getImplementation(classMyDeallocMethod);
//retain
classRetainMethod = class_getInstanceMethod([NSObject class], @selector(retain) );
classRetainImp = method_getImplementation(classRetainMethod);
classMyRetainMethod = class_getInstanceMethod([NSObject class], @selector(myRetainFunc) );
classMyRetainImp = method_getImplementation(classMyRetainMethod);
//release
classReleaseMethod = class_getInstanceMethod([NSObject class], @selector(release) );
classReleaseImp = method_getImplementation(classReleaseMethod);
classMyReleaseMethod = class_getInstanceMethod([NSObject class], @selector(myReleaseFunc) );
classMyReleaseImp = method_getImplementation(classMyReleaseMethod);
ALLOC_METHOD_EXCHANGE;
method_exchangeImplementations(classDeallocMethod, classMyDeallocMethod);
RETAIN_METHOD_EXCHANGE;
method_exchangeImplementations(classReleaseMethod, classMyReleaseMethod);
}
В своем методе я вызываю базовый передавая как и obj_msgSend первые два параметра id и SEL чтобы получить новый объект, если он новый то сохраняю его в глобальный массив memData в самое начало не увеличивая счетчика ссылок. Для получения callstack-а генерируется исключение и тут же ловится. На производительности в симуляторе это сказывается не сильно. ALLOC_METHOD_EXCHANGE вызывается для того чтобы при создании NSMemCheckObject и NSException не уйти в рекурсию.
+ (id) myAllocFunc
{
//call base implement
OverrideMemCheckPrototipe f = (OverrideMemCheckPrototipe)classAllocImp;
id newPt = f(self,@selector(myAllocFunc));
@synchronized( [NSObject class] )
{
if( ![newPt isKindOfClass:[NSMemCheckObject class]] )
{
BOOL found = NO;
for( NSMemCheckObject* obj in memData )
if( obj.pointerValue == newPt )
{
found = YES;
break;
}
if( !found )
{
ALLOC_METHOD_EXCHANGE;
NSMemCheckObject* addObj = [[[NSMemCheckObject alloc] initWithPointer:newPt] autorelease];
[memData insertObject:addObj atIndex:0];
//hack to get call stack
@try
{
@throw [NSException exceptionWithName:@"memTestException"
reason:@"get call stack"
userInfo:nil];
}
@catch (NSException * e)
{
addObj.callStack = [e callStackSymbols];
}
ALLOC_METHOD_EXCHANGE;
}
}
}
return newPt;
}
В реализации myDeallocFunc происходит простое удаление элемента из массива
- (void)myDeallocFunc
{
@synchronized( [NSObject class] )
{
int i = [memData count]-1;
while( i>=0 )
{
if( ((NSMemCheckObject*)[memData objectAtIndex:i]).pointerValue == self )
{
[memData removeObjectAtIndex:i];
//ShowMemData();
break;
}
--i;
}
}
//call base implement
OverrideMemCheckPrototipe f = (OverrideMemCheckPrototipe)classDeallocImp;
f(self,@selector(myDeallocFunc));
}
В myRetainFunc необходимо проверять какая имплементация находится в alloc, чтобы правильно подменить на оригинальную:
- (id)myRetainFunc
{
@synchronized( [NSObject class] )
{
BOOL needAllocExchange = ( method_getImplementation(classAllocMethod) == classMyAllocImp );
if( needAllocExchange )
ALLOC_METHOD_EXCHANGE;
RETAIN_METHOD_EXCHANGE;
NSMemCheckObject* addObj = [memData memCheckObjectByPointer:self];
//hack to get call stack
if( addObj )
{
@try
{
@throw [NSException exceptionWithName:@"memTestException"
reason:@"get call stack"
userInfo:nil];
}
@catch (NSException * e)
{
NSMemCheckRetainReleaseInfo* info = [[NSMemCheckRetainReleaseInfo alloc] init];
info.date = [NSDate date];
info.callStack = [e callStackSymbols];
[addObj.retainCallStackArray addObject:info];
[info release];
}
}
if( needAllocExchange )
ALLOC_METHOD_EXCHANGE;
RETAIN_METHOD_EXCHANGE;
}
//call base implement
OverrideMemCheckPrototipe f = (OverrideMemCheckPrototipe)classRetainImp;
return f(self,@selector(myRetainFunc));
}