简体   繁体   中英

A strange issue with NSMutableArray vs NSMutableDictionary

I have an XML-parser which parses all the information from XML-file (attached as a resource of project) to a collection. First time I did it via NSMutableArray which I converted then to an NSMutableDictionary and everything works fine. After that I realized that its not necessary to have a NSMutableArray; I just can parse XML directly to NSMutableDictionary. And I did it (its just a matter of 1 property and the way I add an object to a collection) but the problem is I got leaks now. Very strange leaks. It could happen and could not. Every time it happens it says that its something about hashing and key/value issue. Parts of code:

- (void)parser:(NSXMLParser *)parser didStartElement:(NSString *)elementName namespaceURI:(NSString *)namespaceURI qualifiedName:(NSString *)qName attributes:(NSDictionary *)attributeDict{
        self.currentElement = elementName;
        // category
        if(self.targetBranch==XML_KEY_CATEGORY && [elementName isEqualToString:XML_KEY_CATEGORY]) {
            self.categoryItem = [[CategoryItem alloc] initWithId:[attributeDict objectForKey:@"id"] andParentId:[attributeDict objectForKey:@"parentId"]];
            //NSLog(@"didStartElement with id %@",self.categoryItem.categoryId);
        }
    ...
        }

        - (void)parser:(NSXMLParser *)parser foundCharacters:(NSString *)string{
        ...
            // categories
            if (self.targetBranch==XML_KEY_CATEGORY && [self.currentElement isEqualToString:XML_KEY_CATEGORY]) {
                //NSLog(@"adding category to array");
                if (self.categoryItem.categoryName)
                    self.categoryItem.categoryName = [NSString stringWithFormat:@"%@%@",self.categoryItem.categoryName,string]; //[string copy];
                else
                    self.categoryItem.categoryName = string; //[string copy];
        ...
        }


        - (void)parser:(NSXMLParser *)parser didEndElement:(NSString *)elementName namespaceURI:(NSString *)namespaceURI qualifiedName:(NSString *)qName{
            //NSLog(@"ended element: %@", elementName);

            // category
            if (self.targetBranch==XML_KEY_CATEGORY && [elementName isEqualToString:XML_KEY_CATEGORY]) {
                //NSLog(@"adding category to array");
                //[xmlParsedInfo addObject:self.categoryItem];
                //      if (self.categoryItem.categoryId && self.categoryItem.categoryId && self.categoryItem.categoryId!=NULL 
        //      && ![xmlParsedData objectForKey:self.categoryItem.categoryId]){
        //          //NSLog(@"adding dictionary element %@",self.categoryItem.categoryId);
        //          [xmlParsedData setObject:self.categoryItem forKey:self.categoryItem.categoryId];
                    [xmlParsedInfo addObject:self.categoryItem];

                }
        ...
        }

        -(NSDictionary*) getCategoriesForParentId:(NSString*)parentId_ fromUrl:(NSString *)strUrl{
            NSLog(@"getCategoriesForParentId: %@",parentId_);
            targetBranch = XML_KEY_CATEGORY;
            parentId = parentId_;
            xmlParsedInfo = [[NSMutableArray alloc] init];
        //  xmlParsedData = [[NSMutableDictionary alloc] init];
            [self parseXMLFileAtURL:strUrl];
            NSLog(@"category for parent id count is %i",xmlParsedInfo.count);
            //return xmlParsedData;
            NSDictionary *result = [self convertCategoryItemArrayToDictionary:xmlParsedInfo];
            [xmlParsedInfo release];
            return result; //[self convertCategoryItemArrayToDictionary:xmlParsedInfo];
        }

        - (NSDictionary *) convertCategoryItemArrayToDictionary:(NSMutableArray *)array 
        {
            NSMutableDictionary *mutableDictionary = [[NSMutableDictionary alloc] init];
            for (CategoryItem *catItem in array) {
                [mutableDictionary setObject:catItem forKey:catItem.categoryId];
            }
            return (NSDictionary *)[mutableDictionary autorelease];
        }

Like I said XML-parser works fine and there are no issues yet. But you can see commented section which are dictionary related. At current state it works good but if comment array related code and UNcomment dictionary related - it will leak. Most of time I'm getting 2 leaks like:

#   Category    Event Type  Timestamp   RefCt   Address Size    Responsible Library Responsible Caller
0   CFBasicHash (key-store) Malloc  1928615936  1   0x9019600   512 MyApp   -[XMLParser parser:didEndElement:namespaceURI:qualifiedName:]
#   Category    Event Type  Timestamp   RefCt   Address Size    Responsible Library Responsible Caller
0   CFBasicHash (value-store)   Malloc  1928608768  1   0x900c600   512 MyApp   -[XMLParser parser:didEndElement:namespaceURI:qualifiedName:]

and those leaks leads directly to the following line of code:

        [xmlParsedData setObject:self.categoryItem forKey:self.categoryItem.categoryId];

So what is the problem with dictionary here? As I can see its hash/key related and its NSMutableDictionary internal issue.

I'm trying the code with emulator only. So I can't say if the issue still persists with real device. Maybe its just emulator related bug (like keyboard leak).
The XML I'm parsing is static and it just a file in a project. There are 71 categories in this XML file. So every run app does the same with the same data. That is why Im saying that the issue is strange. At the first run with leaks monitor there are no any leaks. At the second try it could be 1 or 2. All the next runs it shows constantly 2 (shown above) leaks (+1 leading to the dictionary itself but this is due to its key or value leaks, its just an end of chain).

CategoryItem.h
@interface CategoryItem : NSObject {
    NSString * categoryName;
    NSString * categoryId;
    NSString * parentId;
}
@property (nonatomic,retain) NSString * categoryName; 
@property (nonatomic,retain) NSString * categoryId; 
@property (nonatomic,retain) NSString * parentId; 

- (id)init;
- (id)initWithId:(NSString *)id_ andParentId:(NSString *)parentId_;
- (id)initWithName:(NSString *)name_ andId:(NSString *)id_ andParentId:(NSString *)parentId_;
- (BOOL)isEqualToItem:(CategoryItem *)anItem;
- (NSUInteger)hash;

@end

CategoryItem.m

#import "CategoryItem.h"

@implementation CategoryItem

@synthesize categoryName; 
@synthesize categoryId; 
@synthesize parentId; 

-(id)init{
    self = [super init];
    return self;
}

-(id)initWithName:(NSString *)name_ andId:(NSString *)id_ andParentId:(NSString *)parentId_{
    self = [super init];
    if(self){
        self.categoryName = name_;
        self.categoryId = id_;
        self.parentId = parentId_;
    }
    return self;
}

-(id)initWithId:(NSString *)id_ andParentId:(NSString *)parentId_{
    self = [super init];
    if(self){
        self.categoryId = id_;
        self.parentId = parentId_;
    }
    return self;
}

- (void)dealloc {
    [categoryName release];
    [categoryId release];
    [parentId release];
    [super dealloc];
}

- (BOOL)isEqual:(id)other {
    if (other == self)
        return YES;
    if (!other || ![other isKindOfClass:[self class]])
        return NO;
    return [self isEqualToItem:other];
}

- (BOOL)isEqualToItem:(CategoryItem *)anItem {
    if (self == anItem)
        return YES;
    if (![(id) self.categoryName isEqual:anItem.categoryName])
        return NO;
    if (![(id) self.categoryId   isEqual:anItem.categoryId])
        return NO;
    if (![(id) self.parentId     isEqual:anItem.parentId])
        return NO;
    return YES;
}

- (NSUInteger)hash {
    NSString *string4Hash = [NSString stringWithFormat:@"%@%@%@",self.categoryName,self.categoryId,self.parentId];
    NSUInteger hash = [string4Hash hash];
    return hash;
}

@end

Added: The solution I found is (stupid as for me) in my -(NSDictionary*) getCategoriesForParentId:

return this:

    NSDictionary *result;

    result = (NSDictionary *)[xmlParsedData copy];
    [xmlParsedData release];

    return result;

!OR!

    return [(NSDictionary *)xmlParsedData autorelease];

}

so the point is to copy NSDictionaryMutable... I just can't figure out why it does help?! Not retain but copy! Method which calls getCategoriesForParentId() expects NSDictionary (not Mutable one). It creates XMLParser via alloc+init and after getting the dictionary it releases created XMLParser which has dealloc with

- (void)dealloc {
    [currentElement release];
    [targetBranch   release];

    xmlParsedInfo = nil;
    xmlParsedData = nil; 
...

I also tried to return something like (NSDictionary *)[xmlParsedData retain] or (NSDictionary *)xmlParsedData without additional NSDictionary *result - but leaks was there.

Looks like the problem is I'm not using xmlParsedData as a property+@synthesize. So now I made it a property (retain) and in dealloc its now [xmlParsedData release] and I return [xmlParsedData retain] from getCategoriesForParentId. It still leaks.

Your code seems not to use ARC but please specify it in the question.

You will leak the memory every time you enter into the if branch of your didStartElement and assign a new element to self.categoryItem . You are allocating a new object, but when you assign it to the property it will issue one more retain on it. The next time when you assign the next object, the property will issue one release but the reference counter will be still one more then necessary since your alloc .

To make it work well, issue an autorelease at the creation time or a release after you assigned it to the property:

self.categoryItem = [[[CategoryItem alloc] 
         initWithId:[attributeDict objectForKey:@"id"] 
        andParentId:[attributeDict objectForKey:@"parentId"]] autorelease];

I don't know if this is the problem, but... can you post the categoryItem class? Or... have you implemented the hash method on it?

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM