Objective-C ARC and passing C arrays of objects

2020-06-16 05:16发布

问题:

I'm sorry if this is a bit of a C-noob question: I know I need to swot up on my pointers. Unfortunately I'm on a deadline so don't have time to work through a whole book chapter, so I'm hoping for a bit more targeted advice.

I want to store some objective-C objects in a C array. I'm using ARC. If I were on the Mac I'd be able to use NSPointerArray instead, but I'm on iOS and that's not available.

I'll be storing a three-dimensional C array: conceptually my dimensions are day, height, and cacheNumber. Each element will either be a pointer to an objective-C object, or NULL.

The number of caches (i.e. the size of the cacheNumber dimension) is known at compile time, but the other two are not known. Also, the array could be very large, so I need to dynamically allocate memory for it.

Regarding ownership semantics, I need strong references to the objects.

I would like the whole three-dimensional array to be an instance variable on an objective-C object.

I plan to have a method that is - tableForCacheNumber:(int)num days:(int*)days height:(int*)height. That method should return a two-dimensional array, that is one specific cache number. (It also passes back by reference the size of the array it is returning.)

My questions:

  • What order should I put my dimensions so that I can easily return a pointer to the subarray for one specific cache number? (I think it should be first, but I'm not 100%.)

  • What should the return type of my method be, so that ARC doesn't complain? I don't mind if the returned array has an increased reference count or not, as long as I know which it's doing.

  • What type should my instance variable that holds the three dimensional array be? I think it should just be a pointer, since that ivar just represents the pointer to the first item that's in my array. Correct? If so, how do I specify that?

  • When I create the three-dimensional array (for my ivar), I guess I do something like calloc(X * Y * Z, sizeof(id)), and cast the result to the type for my ivar?

  • When accessing items from the three-dimensional array in the ivar, I believe I have to dereference the pointer each time, with something like (*myArray)[4][7][2]. Correct?

  • Will the two-dimensional array I return from the method be similarly accessed?

  • Do I need to tag the returned two-dimensional array with objc_returns_inner_pointer?

I'm sorry once again that this is a bit of a bad Stack Overflow question (it's too long and with too many parts). I hope the SO citizens will forgive me. To improve my interweb karma, maybe I'll write it up as a blog post when this project has shipped.

回答1:

First off: while you don't have NSPointerArray, you do have CFMutableArrayRef and you can pass any callbacks you want for retain/release/description, including NULL. It may be easier (and performance is something you can measure later) to try that first.

Taking your points in order:

  • you should define your dimensions as [cacheNumber][days][height], as you expect. Then cache[cacheNumber] is a two-dimensional array of type id *[][]. As you've said performance is important, be aware that the fastest way to iterate this beast is:

    for (/* cacheNumber loop */) {
     for (/* days loop */) {
      for (/* height loop */) {
       //...
      }
     }
    }
    
  • it should be of type __strong id ***: that's a pointer to a pointer to a pointer to id, which is the same as array of (array of (pointer to id)).

  • your ivar needs to be __strong id **** (!), because it's an array of the above things.
  • you guess incorrectly regarding allocating the array.. If you're using a multidimensional array, you need to do this (one dimension elided for brevity):

    - (__strong id * * *)someArray {
        __strong id * * *cache = (__strong id * * *)malloc(x*y*sizeof(void *));
        id hello = @"Hello";
    
        cache[0] = (__strong id * *)malloc(sizeof(void *)); //same for cache[1..x-1]
        cache[0][0] = &hello; // for all cache[x][y]
    
        return (__strong id * * *)cache;
    }
    
  • correct, that is how you use such a pointer.

  • yeah, the two-D array works in the same way, sans the first dimension.
  • I don't think so, you're handing out __strong object pointers so you should be grand. That said, we're at about the limit of my ability with this stuff now so I could well be wrong.


回答2:

Answering my own question because this web page gave me the missing bit of info I needed. I've also upvoted Graham's answer, since he was very helpful in getting my head round some of the syntax.

The trick I was missing is knowing that if I want to refer to items in the array via the array[1][5][2] syntax, and that I don't know the sizes of my array at compile time, I can't just calloc() a single block of data for it.

The easiest to read (although least efficient) method of doing that is just with a loop:

__strong Item ****cacheItems;

cacheItems = (__strong Item ****)calloc(kMaxZooms, sizeof(Item ***));

for (int k = 0; k < kMaxZooms; k++) 
{
    cacheItems[k] = (__strong Item ***)calloc((size_t)daysOnTimeline, sizeof(Item **));

    for (int j = 0; j < daysOnTimeline; j++) 
    {
        cacheItems[k][j] = (__strong Item **)calloc((size_t)kMaxHeight, sizeof(Item *));
    }
}

I'm allocating a three dimensional array of Item *s, Item being an objective-C class. (I have of course left out the error handling code in this snippet.)

Once I've done that, I can refer to my array using the square brackets syntax:

cacheItems[zoom][day][heightToUse] = item;

The web page I linked to above also describes a second method for performing the memory allocations, that uses only one call to calloc() per dimension. I haven't tried that method yet, as the one I've just described is working well enough at the moment.



回答3:

I would think of a different implementation. Unless it is a demonstrable (i.e. you have measured and quantified it) performance issue, trying to store Objective-C objects in plain C arrays is often a code smell.

It seems to me that you need an intermediate container object which we will call a Cache for now. One instance will exist for each cache number, and your object will hold an NS(Mutable)Array of them. Cache objects will have properties for the maximum days and height.

The Cache object would most easily be implemented with an NSArray of the objects in it, using simple arithmetic to simulate two dimensions. Your cache object would have a method -objectAtDay:Height: to access the object by its coordinates.

This way, there is no need at all to worry about memory management, ARC does it for you.

Edit

Given that performance is an issue, I would use a 1D array and roll my own arithmetic to calculate offsets. The type of your instance variable would be:

__strong id* myArray;

You can only use C multilevel subscripts (array[i][j][k]) if you know the range of all the dimensions (except the first one). This is because the actual offset is calculated as

(i * (max_j * max_k) + j * max_k + k) * sizeof(element type)

If the compiler doesn't know max_j and max_k, it can't do it. That's precisely the situation you are in.

Given that you have to use a 1D array and calculate the offsets manually, the Apple example will work fine for you.