没有僵尸或未初始化变量的神秘EXC_BAD_ACCESS

时间:2011-06-23 14:06:49

标签: objective-c

我的应用程序在以下行崩溃:

sqlite3_prepare_v2(db, [sql UTF8String], -1, &pStmt, 0);

在FMDB sqlite包装器的方法中:

- (FMResultSet *)executeQuery:(NSString *)sql withArgumentsInArray:(NSArray*)arrayArgs orVAList:(va_list)args {

if (![self databaseExists]) {
    return 0x00;
}

if (inUse) {
    [self warnInUse];
    return 0x00;
}

[self setInUse:YES];

FMResultSet *rs = nil;

int rc                  = 0x00;
sqlite3_stmt *pStmt     = 0x00;
FMStatement *statement  = 0x00;

if (traceExecution && sql) {
    NSLog(@"%@ executeQuery: %@", self, sql);
}

if (shouldCacheStatements) {
    statement = [self cachedStatementForQuery:sql];
    pStmt = statement ? [statement statement] : 0x00;
}

int numberOfRetries = 0;
BOOL retry          = NO;

if (!pStmt) {
    do {
        retry   = NO;
        const char *sqlStatement = [sql UTF8String];
        rc      = sqlite3_prepare_v2(db, sqlStatement, -1, &pStmt, 0);

        if (SQLITE_BUSY == rc || SQLITE_LOCKED == rc) {
            retry = YES;
            usleep(20);

            if (busyRetryTimeout && (numberOfRetries++ > busyRetryTimeout)) {
                NSLog(@"%s:%d Database busy (%@)", __FUNCTION__, __LINE__, [self databasePath]);
                NSLog(@"Database busy");
                sqlite3_finalize(pStmt);
                [self setInUse:NO];
                return nil;
            }
        }
        else if (SQLITE_OK != rc) {


            if (logsErrors) {
                NSLog(@"DB Error: %d \"%@\"", [self lastErrorCode], [self lastErrorMessage]);
                NSLog(@"DB Query: %@", sql);
#ifndef NS_BLOCK_ASSERTIONS
                if (crashOnErrors) {
                    NSAssert2(false, @"DB Error: %d \"%@\"", [self lastErrorCode], [self lastErrorMessage]);
                }
#endif
            }

            sqlite3_finalize(pStmt);

            [self setInUse:NO];
            return nil;
        }
    }
    while (retry);
}

id obj;
int idx = 0;
int queryCount = sqlite3_bind_parameter_count(pStmt); // pointed out by Dominic Yu (thanks!)

while (idx < queryCount) {

    if (arrayArgs) {
        obj = [arrayArgs objectAtIndex:idx];
    }
    else {
        obj = va_arg(args, id);
    }

    if (traceExecution) {
        NSLog(@"obj: %@", obj);
    }

    idx++;

    [self bindObject:obj toColumn:idx inStatement:pStmt];
}

if (idx != queryCount) {
    NSLog(@"Error: the bind count is not correct for the # of variables (executeQuery)");
    sqlite3_finalize(pStmt);
    [self setInUse:NO];
    return nil;
}

[statement retain]; // to balance the release below

if (!statement) {
    statement = [[FMStatement alloc] init];
    [statement setStatement:pStmt];

    if (shouldCacheStatements) {
        [self setCachedStatement:statement forQuery:sql];
    }
}

// the statement gets closed in rs's dealloc or [rs close];
rs = [FMResultSet resultSetWithStatement:statement usingParentDatabase:self];
[rs setQuery:sql];
NSValue *openResultSet = [NSValue valueWithNonretainedObject:rs];
[openResultSets addObject:openResultSet];

statement.useCount = statement.useCount + 1;

[statement release];    

[self setInUse:NO];

return rs;

}

该应用与EXC_BAD_ACCESS崩溃。我试图通过调试NSZombieEnabled和malloc_history找出原因,但它没有给我任何答案。另外 - 调试器告诉我sql变量的保留计数非常大(这可能是因为它是一个静态的NSString) - 所以EXC_BAD_ACCESS不应该是因为sql对象被过度释放。

有没有人对如何进一步调试这个以找出问题是什么有任何想法?

2 个答案:

答案 0 :(得分:3)

解决方案:问题是我的数据库被多个线程访问。并且即使所有线程都具有对数据库句柄的同步访问权限,对于3.3.1之前的sqlite版本(iOS使用3.0),您无法安全地跨线程使用相同的数据库句柄。

我的解决方案是为每个尝试访问数据库的线程创建数据库的按需句柄,如下所示:

- (ADatabaseConnection *)databaseConnection {

NSDictionary *dictionary = [[NSThread currentThread] threadDictionary];
NSString *key = @"aDatabaseConnection";
ADatabaseConnection *connection = [dictionary objectForKey:key];
if (connection == nil) {

    connection = [[[ADatabaseConnection alloc] initWithDatabase:self] autorelease];
    [dictionary setValue:connection forKey:key];
}
return connection;
}

请注意,对于sqlite版本&gt; = 3.3.1,不需要这样做,因为可以跨线程使用相同的句柄。

另一个需要记住的重要事项是,即使您使用此方法安全地跨线程使用相同的数据库,也可以明智地同步对数据库的访问,以便您不会同时访问它以避免数据库锁定错误。我这两个,每个线程使用一个句柄并在数据库上同步。

答案 1 :(得分:2)

同时从多个线程使用FMDatabase是不安全的 - 所以我开始研究一个新类,以帮助您使用池从多个线程进行查询和更新。现在它位于分支上,您可以在此处查看: https://github.com/ccgus/fmdb/tree/threadtests

阅读标题为“使用FMDatabasePool和线程安全”的部分。