Cocoa / ARC - 为什么将结构作为参数传递会导致自动释放池访问错误?

发布于 2024-12-26 05:30:32 字数 9302 浏览 4 评论 0 原文

我刚刚完成了项目的调试,发现它在模拟器上工作正常,但在使用 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 文件的名称。

有了这个解释,我将描述我如何在我的项目中处理这个问题:

  1. 用户使用上述方法 -initWithName: File: 实例化一个新的“AEPlayer”类:

  2. 此方法首先打开从 Maya 导出的 .dat 文件,以查找要加载到玩家关节的文件的名称。

  3. 对于 .dat 文件中找到的每个关节名称,系统将使用末尾带有“.obj”的关节名称实例化一个新的 NGLMesh 实例,然后打开当前关节的 .anim 文件。系统还会将实例化的 NGLMesh 添加到玩家的关节数组中。

  4. 对于每个关节的 .anim 文件,它使用 NSScanner 读取该文件。它一次扫描 6 行(每帧的 xyz 位置和 xyz 旋转)。扫描完一帧数据后,它实例化一个名为 AEFrame 的类,并将该帧的位置数据设置为加载的位置数据,将旋转数据设置为加载的旋转数据。这是我在将 xyz 位置结构和 xyz 旋转结构传递到其初始化参数时遇到问题的类,但通过传递 6 个 GLfloats 来“修复”,如上面的代码所示。

  5. 当帧被实例化时,该帧被添加到当前玩家的动画数组(一个 NSMutableArray)中。该动画数组最终会变得相当大,因为它存储了模型中每个关节的所有动画数据。我正在测试的模型有 42 个关节,每个关节有 12 个动画帧,每个帧有 6 个 GLfloats。

  6. 当系统完成将 .dat 文件中找到的所有 .obj 文件及其关联的 .obj 和 .anim 文件加载到内存中时,就完成了。

  7. 在程序的主运行循环中,我简单地循环遍历所有创建的 AEPlayer,并且对于每个 AEPlayer,我循环遍历玩家的关节数组,将存储在当前玩家关节处的 NGLMesh 的位置和旋转设置为任意值位置/旋转可在当前关节的播放器动画数组中找到。

我意识到让任何人阅读所有这些内容的可能性不大,但我想我应该把它扔在那里。另外,我知道上面的代码片段中有一些死分配需要删除,但它们不会导致问题(我在问题出现后添加它们,以查看使用 Instruments 的内存情况) 。

非常感谢您再次提供帮助, - 亚当·艾斯菲尔德

I just finished debugging the hell out of my project after I found it was working fine on the simulator but would crash when tested on a device with an EXC BAD ACCESS error at the @autoreleasepool using ARC.

I finally narrowed the problem down to where I had a custom init method for a custom class I created that accepted two structs as parameters. The structs were based off of the same definition, containing only 3 GLfloats to represent x, y and z data for use with positioning and rotation.

When I modified the custom init method to instead accept 6 GLfloats instead of two structs each containing 3 GLfloats, and then had the init method assign these GLfloats to the two appropriate instance variable structs of the class instead of assigning the previously passed structs directly to the instance variable structs, it all worked fine without error.

For a bit of clarification:

I had a struct defined like so:

struct xyz{
    GLfloat x;
    GLfloat y;
    GLfloat z;
};

I then had two ivars of a custom class (lets call it foo) called position and rotation, both based off of this struct.

I then had a custom init method simplified to this:

-(id) initWithPosition: (struct xyz) setPosition rotation: (struct xyz) setRotation
{
    self = [super init];
    if(self) {
        position = setPosition;
        rotation = setRotation;
    }
    return self;
}

Which I called using something similar to:

struct xyz position;
struct xyz rotation;

// Fill position / rotation with data here....

foo *bar = [[foo alloc] initWithPosition: position rotation: rotation];

Which resulted in the EXC BAD ACCESS. So I changed the init method to this:

-(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;
}

And... all is well.

I mean, Im glad I "fixed" it, but Im not sure why I fixed it. Ive read that ARC has issues with using structs of objects, but my structs are only comprised of simple GLfloats, which aren't objects... right? Id rather know why this is working before I move on, and hopefully help some others out there experiencing the same problem.

Thanks,
- Adam Eisfeld

EDIT: Adding Source

Try as I might I cant seem to duplicate the issue in a test project, so there must be something wrong with my code as others have suggested and I haven't fixed the problem at all, only masked it until later, which is what I was afraid of.

So if anyone can take a look at this code to point out what might be going wrong here (if the issue is indeed in the init method, which maybe its not), Id greatly appreciate it. Its rather large so I understand if nobody is interested, but Ive commented it and Ill explain whats going on in further detail after the code snippet:

-(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;
}

Alright so essentially I am writing the code that handles animating the 3D joints of a given player in my engine. I wrote a script for Maya in MEL that allows you to select a series of animated models in the viewport (in this case the joints of a robot character IE the upper and lower arms, the head, the torso, etc) and then run the script which will go through each selected model and export a .anim file. This .anim file contains the xyz position and rotation of the joint the file refers to at each frame of it's animation, so that it is structured like this:

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...

However this animation file consists purely of the floats refering to the xyz positions and rotations, there is no actual text labeling each line as shown above, thats only for reference.

When the exporting of .anim files is done for each selected joint of the animated character, the script exports one final file with a .dat extension. This file contains the amount of animations exported (this value is set in the script, for example you might be exporting 3 animations to a .anim file such as Run, Walk and Idle), it then contains a list of names. These names refer to both the names of the .obj files to load into the 3D engine and also the corresponding .anim files to load for the loaded .obj files.

With that explained, Ill describe how Im handling this in my project:

  1. The user instantiates a new "AEPlayer" class using the above method -initWithName: File:

  2. This method first opens up the .dat file exported from maya to find the names of the files to load in for the player's joints.

  3. For every joint name found in the .dat file, the system proceeds to then instantiate a new NGLMesh instance using the joint's name with a ".obj" at the end of it, and then open up the current joint's .anim file. The system also add's the instantiated NGLMesh to the player's joints array.

  4. For each joint's .anim file, it reads through the file using an NSScanner. It scans in 6 lines at a time (the xyz position and xyz rotation of each frame). After scanning in one frame's data, it instantiates a class called AEFrame and sets the frame's position data to the loaded position data and rotation data to the loaded rotation data. This is the class that I was having trouble with passing the xyz position struct and xyz rotation struct to in it's init parameters but "fixed" by passing in 6 GLfloats instead, as shown in the above code.

  5. When the frame has been instantiated, this frame is added to the current player's animations array (an NSMutableArray). This animations array ends up getting fairly large, as it stores all of the animation data for every joint in the model. The model I am testing with has 42 joints, each joint has 12 frames of animation, each frame has 6 GLfloats.

  6. When the system is finished loading all of the .obj files found in the .dat file and their associated .obj and .anim files into memory, it is finished.

  7. In the main run loop of the program, I simply loop through all of the created AEPlayers, and for each AEPLayer I loop through the player's joints array, setting the position and rotation of the NGLMesh stored at the current player's joint to whatever position / rotation is found in the player's animations array for the current joint.

I realize its a long shot to get anyone to read all of that, but I thought Id throw it out there. Also, Im aware that I have a few dead allocations in the above snippet that I need to get rid of, but they're not contributing to the problem (I added them after the problem arose to see what was happening memory wise using Instruments).

Thanks a lot for any help again,
- Adam Eisfeld

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(1

决绝 2025-01-02 05:30:32

其他一些代码可能会破坏堆栈,或者执行其他未定义的操作。如果您正在做类似的事情(调试起来可能非常棘手),那么所有的赌注都会被取消,简单的更改可能会掩盖或揭露真正的错误。

当您在设备和模拟器之间切换时,您也在为 ARM 而不是 i386 进行构建,并且正在构建优化而不是调试。这些都是巨大的变化,可能会改变未定义行为的结果。例如,在数组末尾写入一个字节可能在调试版本中不会造成任何损害,但在发布版本中会导致灾难性的失败。将结构更改为浮点数可能会更改参数传递到方法中的方式,这可能会掩盖或揭露错误。

您应该尝试将您的问题隔离到一个小型示例项目中。例如,您可以仅使用一个类、仅使用一个 init 方法来重现该问题吗?

如果您还不熟悉此主题,来自 LLVM 博客的这组文章可能会有所帮助:

更新

看来您的问题可能在这里:

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];

-[NSString initWithContentsOfFile:usedEncoding:error] 是一个输出参数,这意味着您提供存储并将指针传递给该存储,该函数将向其中写入一些输出。您提供了一个指针,但它没有指向任何有效的内容,并且当 -[NSString initWithContentsOfFile:usedEncoding:error] 写入usedEncoding 指针时,它将覆盖某些内容。

您应该将该代码更改为:

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];

现在您已经提供了一个指向有效字符串编码变量的指针。

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:

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];

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:

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];

Now you've provided a pointer to a valid string encoding variable.

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文