Not sure if this applies to iOS since Unity uses IL2CPP, but I would assume the same principle applies here as it does in Mono.
So at a guess and assuming I’ve understood how Mono works, the problem you might be facing is that GC.Collect does not release/return memory to the system device, but simply marks it as unused and free to be used by future allocations.
The gotcha arises when due to your apps memory usage pattern the ‘free’ memory becomes fragmented. When this happens and there is no continuous amount of memory free for new allocations, Unity will grab a fresh new block from the system.
So for example lets say you grab a textures pixels into a byteArray for a 10MB texture. Unity will need to allocate a continuous block of 10MB for it. Later you destroy the byteArray after working on it and call gc.collect. Unity will mark that 10MB block of memory as being free.for future use, but its never returned to the system.
Now lets say you have a series of allocations for other work amounting to 5MB in total leaving 5MB continuous memory free ( though possibly not at the end of the block). Then you grab the bytearray of the textures pixels again. Best case Unity has to allocate an additional 5MB to provide a continuous 10MB block to store your byte array, worse case it allocates a whole new 10MB block, meaning you’ve now used 20MB in total!
Now repeat this process a few times or even every frame and its clear that eventually you will simply run out of memory.
The solutions are very much project dependent and especially its memory usage pattern. For that you’ll have to dig through your code and try and see what is going on. Various strategies exist such as object pooling, even as simple as if you know you are repeatedly going to need 10MB of ram for a byteArray to never destroying the reference to it and just re-use it.
Another option might be to go the ‘unsafe’ route ( unsure how that applies to IL2CPP ) and allocate memory yourself, as it is never in managed code, it never goes through garbage collection and so should be released back to the system, but that doesn’t always solve fragmentation issues. It just gives you a bit more leeway and control.
Of course alternatively there might be a bug in your code or Unity that is needless allocating memory every frame In which case you’ll have to track that down.
Finally one other important aspect to check is whether you might not be unloading assets that you think should be unloaded. Go grab the Unity MemoryProfiler from bitbucket. Read the documentation then try it on your project. Last time I used it, getting it to work was a bit hit or miss, but eventually you should be able to work it out.
What you might find is that static references can keep asset references alive when you think they would be unloaded. In fact its can be quite an eye opener to see just how easy a cascade effect of static references can end up causing a number of assets to never be unloaded. For example from memory I think if a gameObject contains several script components, and one of those uses a static reference then the entire gameObject is marked as static and so other references in other scripts on the gameObject can become marked as static too - at least it was something like that, or maybe use of ‘DontDestroyOnLoad’ affected all scripts on a gameObject.
I do remember being able to cut memory usage down by 5 - 10MB which on old 512Mb iOS devices using iOS8 and upwards could make all the difference between the app working or crashing.
Hope this helps.