воскресенье, 20 февраля 2011 г.

Новая версия библиотеки для поиска утечек памяти в iOS приложениях

Краткое описание

В процессе разработки приложений для 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));

}

Комментариев нет:

Отправить комментарий