Cocoa / ARC - 为什么将结构作为参数传递会导致自动释放池访问错误?
我刚刚完成了项目的调试,发现它在模拟器上工作正常,但在使用 ARC 的 @autoreleasepool 出现 EXC BAD ACCESS 错误的设备上进行测试时会崩溃。
我最终将问题缩小到我创建的自定义类有一个自定义 init 方法,该方法接受两个结构作为参数。这些结构基于相同的定义,仅包含 3 个 GLfloats 来表示用于定位和旋转的 x、y 和 z 数据。
当我修改自定义 init 方法以接受 6 个 GLfloats 而不是两个各自包含 3 个 GLfloats 的结构,然后让 init 方法将这些 GLfloats 分配给该类的两个适当的实例变量结构,而不是将先前传递的结构直接分配给实例变量结构,一切正常,没有错误。
为了澄清一点:
我有一个像这样定义的结构:
struct xyz{
GLfloat x;
GLfloat y;
GLfloat z;
};
然后我有一个自定义类(我们称之为 foo)的两个 ivars,称为位置和旋转,两者都基于这个结构。
然后,我将自定义 init 方法简化为:
-(id) initWithPosition: (struct xyz) setPosition rotation: (struct xyz) setRotation
{
self = [super init];
if(self) {
position = setPosition;
rotation = setRotation;
}
return self;
}
我使用类似于以下内容的方法进行调用:
struct xyz position;
struct xyz rotation;
// Fill position / rotation with data here....
foo *bar = [[foo alloc] initWithPosition: position rotation: rotation];
这导致了 EXC BAD ACCESS。所以我将 init 方法更改为:
-(id) initWithPositionX: (GLfloat) xp Y: (GLfloat) yp Z: (GLfloat) zp RotationX: (GLfloat) xr Y: (GLfloat) yr Z: (GLfloat) zr
{
self = [super init];
if(self) {
position.x = xp;
position.y = yp;
position.z = zp;
rotation.x = xr;
rotation.y = yr;
rotation.z = zr;
}
return self;
}
并且......一切都很好。
我的意思是,我很高兴我“修复”了它,但我不确定为什么要修复它。我读到 ARC 在使用对象结构时存在问题,但我的结构仅由简单的 GLfloats 组成,它们不是对象......对吗?在我继续之前,我宁愿知道为什么这是有效的,并希望能帮助其他遇到同样问题的人。
谢谢, - Adam Eisfeld
编辑:添加源
尽我所能尝试,我似乎无法在测试项目中重复该问题,因此我的代码一定有问题,正如其他人所建议的那样,而且我还没有修复根本没有问题,只是掩盖了它,直到后来,这正是我所担心的。
因此,如果有人可以看一下这段代码,指出这里可能出了什么问题(如果问题确实出在 init 方法中,但可能不是),我将非常感激。它相当大,所以我知道如果没有人感兴趣,但我对其进行了评论,并且我将在代码片段之后更详细地解释发生了什么:
-(id) initWithName: (NSString*) setName fromFile: (NSString*) file
{
self = [super init];
if(self) {
//Initialize ivars
name = setName;
joints = [[NSMutableArray alloc] init];
animations = [[NSMutableArray alloc] init];
animationCount = 0;
frameCount = 0;
//Init file objects for reading
NSError *fileError;
NSStringEncoding *encoding;
//Load the specified file's contents into an NSString
NSString *fileData = [[NSString alloc] initWithContentsOfFile:[[NSBundle mainBundle] pathForResource:file ofType:@"dat"] usedEncoding:encoding error:&fileError];
//Create a new NGLMaterial to apply to all of the loaded models from the file
NGLMaterial *material = [[NGLMaterial alloc] init];
material = [NGLMaterial material];
material.diffuseMap = [NGLTexture texture2DWithFile:@"resources/models/diffuse.bmp"];
//Check for nil file data
if(fileData == nil)
{
NSLog(@"Error reading mesh file");
}
else
{
//Separate the NSString of the file's contents into an array, one line per indice
NSMutableArray *fileLines = [[NSMutableArray alloc] initWithArray:[fileData componentsSeparatedByString:@"\n"] copyItems: YES];
//Create a pseudo counter variable
int i = -1;
//Allocate a nul NSString for looping
NSString *line = [[NSString alloc] initWithFormat:@""];
//Loop through each of the lines in the fileLines array, parsing the line's data
for (line in fileLines) {
//Increase the pseudo counter variable to determine what line we're currently on
i++;
if (i == 0) {
//The first line of the file refers to the number of animations for the player
animationCount = [line intValue];
}else
{
if (i == [fileLines count]-2) {
//The last line of the file refers to the frame count for the player
frameCount = [line intValue];
}else
{
//The lines inbetween the first and last contain the names of the .obj files to load
//Obtain the current .obj path by combining the name of the model with it's path
NSString *objPath = [[NSString alloc] initWithFormat:@"resources/models/%@.obj", line];
//Instantiate a new NGLMesh with the objPath NSString
NGLMesh *newMesh = [[NGLMesh alloc] initWithOBJFile:objPath];
//Apply various settings to the mesh such as material
newMesh.material = material;
newMesh.rotationOrder = NGLRotationOrderZYX;
//Compile the changes to the mesh
[newMesh compileCoreMesh];
//Add the mesh to this player's joints array
[joints addObject:newMesh];
//Read the animation data for this joint from it's associated file
NSLog(@"Reading animation data for: %@", line);
//The associated animation file for this model is found at (model's name).anim
NSString *animData = [[NSString alloc] initWithContentsOfFile:[[NSBundle mainBundle] pathForResource:line ofType:@"anim"] usedEncoding:encoding error:&fileError];
//Check for nil animation data
if(animData == nil)
{
NSLog(@"Error reading animation file");
}
else
{
//Construct temporary position and rotation structs to store the read xyz data from each frame of animation
struct xyz position;
struct xyz rotation;
//Create a new scanner to scan the current animation file for it's xyz position / rotation data per frame
NSScanner *scanner = [[NSScanner alloc] initWithString:animData];
while([scanner isAtEnd] == NO)
{
//Extract position data
[scanner scanFloat:&position.x];
[scanner scanFloat:&position.y];
[scanner scanFloat:&position.z];
//Extract rotation data
[scanner scanFloat:&rotation.x];
[scanner scanFloat:&rotation.y];
[scanner scanFloat:&rotation.z];
//OLD CODE NOT WORKING:
//AEFrame *frame = [[AEFrame alloc] initWithPosition: position rotation: rotation];
//Initialize new frame instance using new working init method
AEFrame *frame = [[AEFrame alloc] initWithPositionX:position.x Y:position.y Z:position.z RotationX:rotation.x Y:rotation.y Z:rotation.z];
//Add the created frame instace to the player's animations array
[animations addObject:frame];
}
}
}
}
}
}
}
return self;
}
好吧,本质上我正在编写处理引擎中给定玩家的 3D 关节动画的代码。我用 MEL 为 Maya 编写了一个脚本,允许您在视口中选择一系列动画模型(在本例中为机器人角色的关节,即上臂和下臂、头部、躯干等),然后运行脚本将遍历每个选定的模型并导出 .anim 文件。这个 .anim 文件包含该文件在其动画的每一帧所引用的关节的 xyz 位置和旋转,因此它的结构如下:
Frame 1 X Position
Frame 1 Y Position
Frame 1 Z Position
Frame 1 X Rotation
Frame 1 Y Rotation
Frame 1 Z Rotation
Frame 2 X Position
Frame 2 Y Position
etc...
但是,该动画文件纯粹由引用 xyz 位置和旋转的浮动组成,有如上所示,没有实际的文本标记每一行,仅供参考。
当为动画角色的每个选定关节导出 .anim 文件后,脚本将导出一个扩展名为 .dat 的最终文件。该文件包含导出的动画数量(该值在脚本中设置,例如您可能将 3 个动画导出到 .anim 文件,例如“奔跑”、“行走”和“闲置”),然后它包含一个名称列表。这些名称既指要加载到 3D 引擎中的 .obj 文件的名称,也指要为已加载的 .obj 文件加载的相应 .anim 文件的名称。
有了这个解释,我将描述我如何在我的项目中处理这个问题:
-
用户使用上述方法 -initWithName: File: 实例化一个新的“AEPlayer”类:
-
此方法首先打开从 Maya 导出的 .dat 文件,以查找要加载到玩家关节的文件的名称。
-
对于 .dat 文件中找到的每个关节名称,系统将使用末尾带有“.obj”的关节名称实例化一个新的 NGLMesh 实例,然后打开当前关节的 .anim 文件。系统还会将实例化的 NGLMesh 添加到玩家的关节数组中。
-
对于每个关节的 .anim 文件,它使用 NSScanner 读取该文件。它一次扫描 6 行(每帧的 xyz 位置和 xyz 旋转)。扫描完一帧数据后,它实例化一个名为 AEFrame 的类,并将该帧的位置数据设置为加载的位置数据,将旋转数据设置为加载的旋转数据。这是我在将 xyz 位置结构和 xyz 旋转结构传递到其初始化参数时遇到问题的类,但通过传递 6 个 GLfloats 来“修复”,如上面的代码所示。
-
当帧被实例化时,该帧被添加到当前玩家的动画数组(一个 NSMutableArray)中。该动画数组最终会变得相当大,因为它存储了模型中每个关节的所有动画数据。我正在测试的模型有 42 个关节,每个关节有 12 个动画帧,每个帧有 6 个 GLfloats。
-
当系统完成将 .dat 文件中找到的所有 .obj 文件及其关联的 .obj 和 .anim 文件加载到内存中时,就完成了。
-
在程序的主运行循环中,我简单地循环遍历所有创建的 AEPlayer,并且对于每个 AEPlayer,我循环遍历玩家的关节数组,将存储在当前玩家关节处的 NGLMesh 的位置和旋转设置为任意值位置/旋转可在当前关节的播放器动画数组中找到。
我意识到让任何人阅读所有这些内容的可能性不大,但我想我应该把它扔在那里。另外,我知道上面的代码片段中有一些死分配需要删除,但它们不会导致问题(我在问题出现后添加它们,以查看使用 Instruments 的内存情况) 。
非常感谢您再次提供帮助, - 亚当·艾斯菲尔德
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论
评论(1)
其他一些代码可能会破坏堆栈,或者执行其他未定义的操作。如果您正在做类似的事情(调试起来可能非常棘手),那么所有的赌注都会被取消,简单的更改可能会掩盖或揭露真正的错误。
当您在设备和模拟器之间切换时,您也在为 ARM 而不是 i386 进行构建,并且正在构建优化而不是调试。这些都是巨大的变化,可能会改变未定义行为的结果。例如,在数组末尾写入一个字节可能在调试版本中不会造成任何损害,但在发布版本中会导致灾难性的失败。将结构更改为浮点数可能会更改参数传递到方法中的方式,这可能会掩盖或揭露错误。
您应该尝试将您的问题隔离到一个小型示例项目中。例如,您可以仅使用一个类、仅使用一个 init 方法来重现该问题吗?
如果您还不熟悉此主题,来自 LLVM 博客的这组文章可能会有所帮助:
更新:
看来您的问题可能在这里:
-[NSString initWithContentsOfFile:usedEncoding:error] 是一个输出参数,这意味着您提供存储并将指针传递给该存储,该函数将向其中写入一些输出。您提供了一个指针,但它没有指向任何有效的内容,并且当 -[NSString initWithContentsOfFile:usedEncoding:error] 写入usedEncoding 指针时,它将覆盖某些内容。
您应该将该代码更改为:
现在您已经提供了一个指向有效字符串编码变量的指针。
It's possible some other code is corrupting the stack, or doing something else that's undefined. If you're doing anything like that (which can be really tricky to debug), all bets are off, and simple changes might mask or unmask the real bug.
When you change between device and simulator, you're also building for ARM instead of i386 and are building optimized instead of debug. Those are all huge changes and could change the result of the undefined behavior. For example, writing one byte past the end of an array could result in no harm in a debug build, but catastrophic failure in a release build. Changing the structs to floats could change the way the parameters are passed into your method, which could either mask or unmask the bug.
You should try to isolate your problem to a small sample project. For example, can you reproduce the problem with just one class, with just that one init method?
This set of articles from the LLVM blog might be helpful if you're not already familiar with this topic:
Update:
It looks like your problem may be here:
The usedEncoding parameter of -[NSString initWithContentsOfFile:usedEncoding:error] is an out parameter, meaning that you provide storage and pass a pointer to that storage and the function will write some output to it. You've provided a pointer, but it doesn't point at anything valid, and when -[NSString initWithContentsOfFile:usedEncoding:error] writes to the usedEncoding pointer, it will overwrite something.
You should change that code to this:
Now you've provided a pointer to a valid string encoding variable.