- Home /
IOS memory leak because of WWW object
Hi there,
I have a nifty memory leak when I run my project on the IOS platform. I have checked it using instruments leak tool and debug using Xcode 6. In some cases it looks like the WWW object is never disposed.
I°) Here is the part of code that handle sending WWW requests:
private void issueRequestWithoutQueuing(string url, Dictionary<string, string> parameters = null, Callback<ServerData> successCallback = null, Callback<string> apiFailureCallback = null, Callback<string> layerFailureCallback = null, bool showSpinner = false)
{
QueuedWWWObject queueWWW = new QueuedWWWObject();
queueWWW.url = url;
queueWWW.successCB = successCallback;
queueWWW.apiFailureCB = apiFailureCallback;
queueWWW.layerFailureCB = layerFailureCallback;
if (parameters != null)
{
queueWWW.parameters = parameters;
}
issueRequest(queueWWW, showSpinner);
}
private void issueRequest(QueuedWWWObject queueWWW, bool showSpinner = false)
{
numberRequestInProgress ++;
WWW www;
if (queueWWW.parameters != null && queueWWW.parameters.Count != 0)
{
WWWForm form = new WWWForm();
foreach (KeyValuePair<string, string> pair in queueWWW.parameters)
{
form.AddField(pair.Key, pair.Value);
}
www = new WWW(queueWWW.url, form);
}
else
{
www = new WWW(queueWWW.url);
}
NetworkManager.instance.StartCoroutine(WaitForRequest(www, queueWWW, queueWWW.successCB, queueWWW.apiFailureCB, queueWWW.layerFailureCB));
}
We are waiting for request until we receive either that the request is done, there has been an error, or the request has timed out
private IEnumerator<WWW> WaitForRequest(WWW www, Callback<ServerData> successCallback = null, Callback<string> apiFailureCallback = null, Callback<string> layerFailureCallback = null)
{
int startLaunchRequestTime = ServerTime.instance.getRealGameTime();
while (!www.isDone && www.error == null && ((ServerTime.instance.getRealGameTime() - startLaunchRequestTime) < REQUEST_TIMEOUT))
{
yield return www;
}
LaunchCallback cb = new LaunchCallback();
cb.www = www;
cb.successCB = successCallback;
cb.apiFailureCB = apiFailureCallback;
cb.layerFailureCB = layerFailureCallback;
if ((ServerTime.instance.getRealGameTime() - startLaunchRequestTime) >= REQUEST_TIMEOUT)
{
cb.timeOutError = true;
}
_launchCallbacks.Add(cb);
}
II°) And then, in the update function, where we request the results for the launchcallback, handle the callbacks, and dispose the WWW object:
public void update()
{
int len = _launchCallbacks.Count;
List<LaunchCallback> callbackToDelete = new List<LaunchCallback>();
for (int i=0; i<len; i++)
{
LaunchCallback cb = _launchCallbacks[i];
RequestResult(cb.www, cb.hasTransaction, cb.successCB, cb.apiFailureCB, cb.layerFailureCB, cb.timeOutError);
callbackToDelete.Add(cb);
}
foreach(LaunchCallback call in callbackToDelete)
{
_launchCallbacks.Remove(call);
call.dispose();
}
}
private void RequestResult(WWW www, bool hasTransaction, Callback<ServerData> successCallback, Callback<string> apiFailureCallback , Callback<string> layerFailureCallback, bool timeOutError)
{
// handles success/ failure callbacks, not relevant here
numberRequestInProgress --;
}
III°) The LauncCcallback class is just a container for the WWW object and its callbacks:
public class LaunchCallback
{
public Callback<ServerData> successCB;
public Callback<string> apiFailureCB;
public Callback<string> layerFailureCB;
public WWW www;
public bool hasTransaction;
public bool timeOutError = false;
public void dispose()
{
Debug.Log (" DISPOSING WWW object " + www.url );
www.Dispose();
successCB = null;
apiFailureCB = null;
layerFailureCB = null;
}
}
When I run the project on IOS, I get the debug output of DISPOSING WWW object " + www.url, but the leak instrument tells me that the issueRequest function has leaked several strings, dictionary etc etc, as if the WWW has not been disposed:
When I put a breakpoint in Xcode, we pass in the destroy function for every request:
extern "C" void UnityDestroyWWWConnection(void* connection)
{
UnityWWWConnectionDelegate* delegate = (UnityWWWConnectionDelegate*)connection;
[delegate cleanup];
[delegate release];
}
which calls this:
- (void)cleanup
{
[_connection cancel];
_connection = nil;
[_data release];
_data = nil;
}
I really don't know what's going on, because I do pass in the dispose function of the launchcallback. Is there any references that could lead the WWW object to not be disposed?
PS: I am using unity 4.5.5 & monodevelop 4.0.1
Many thanks,
Down
UPDATE 1:
A little update:
After reviewing the code generated by unity in xcode and analyzing it: xcode tells me that there is a potential leak in the WWWConnection.mm, particulary here, the delegate.connection is never cleaned:
extern "C" void* UnityStartWWWConnectionGet(void* udata, const void* headerDict, const char* url)
{
UnityWWWConnectionDelegate* delegate = [UnityWWWConnectionDelegate newDelegateWithCStringURL:url udata:udata];
NSMutableURLRequest* request =
[UnityWWWConnectionDelegate newRequestForHTTPMethod:@"GET" url:delegate.url headers:(NSDictionary*)headerDict];
delegate.connection = [NSURLConnection connectionWithRequest:request delegate:delegate];
return delegate;
}
the same occurs for POST methods:
extern "C" void* UnityStartWWWConnectionPost(void* udata, const void* headerDict, const char* url, const void* data, unsigned length)
{
UnityWWWConnectionDelegate* delegate = [UnityWWWConnectionDelegate newDelegateWithCStringURL:url udata:udata];
NSMutableURLRequest* request =
[UnityWWWConnectionDelegate newRequestForHTTPMethod:@"POST" url:delegate.url headers:(NSDictionary*)headerDict];
[request setHTTPBody:[NSData dataWithBytes:data length:length]];
[request setValue:[NSString stringWithFormat:@"%d", length] forHTTPHeaderField:@"Content-Length"];
delegate.connection = [NSURLConnection connectionWithRequest:request delegate:delegate];
return delegate;
}
Which correlates nicely with what I see:
I don't really know what to do, as it looks like its related to the unity code. Any help would be appreciated.
UPDATE 2
I have a similar leak when I call this function:
private void OnEnable()
{
StartCoroutine(doGet(facebookURL, onGetFacebookAvatar));
}
public IEnumerator doGet(string url, Callback<WWW> callback = null)
{
using (WWW www = new WWW(url))
{
yield return www;
if (callback != null)
{
callback(www);
callback = null;
}
}
}
private void onGetFacebookAvatar(WWW result)
{
if (!string.IsNullOrEmpty (result.error))
{
ErrorManager.instance.logWarning (ErrorManager.ERROR_FACEBOOK_API, "Get facebook avatar failed : " + result.error, "AvatarBehaviour");
}
else
{
Texture2D texture = new Texture2D(frame.width-20, frame.height-20);
if (!_facebookAvatar.ContainsKey(_userID))
{
// Never use www.texture as it cause memory leak (WWW object is never released)
result.LoadImageIntoTexture(texture);
_facebookAvatar.Add(_userID, texture);
}
avatarTexture.mainTexture = texture;
avatarTexture.width = frame.width - 20;
avatarTexture.height = frame.height - 20;
NGUITools.SetActiveSelf(avatarSprite.gameObject, false);
NGUITools.SetActiveSelf(avatarTexture.gameObject, true);
}
}
UPDATE 3:
After some investigation, adding the line:
[request release];
here
extern "C" void* UnityStartWWWConnectionGet(void* udata, const void* headerDict, const char* url)
{
UnityWWWConnectionDelegate* delegate = [UnityWWWConnectionDelegate newDelegateWithCStringURL:url udata:udata];
NSMutableURLRequest* request =
[UnityWWWConnectionDelegate newRequestForHTTPMethod:@"GET" url:delegate.url headers:(NSDictionary*)headerDict];
delegate.connection = [NSURLConnection connectionWithRequest:request delegate:delegate];
[request release];
return delegate;
}
Does helps, but still some leak, maybe because what's inside the request is not fully released (string and stuff), still investigating
while (!www.isDone && www.error == null && ((ServerTime.instance.getRealGameTime() - startLaunchRequestTime) < REQUEST_TI$$anonymous$$EOUT))
{
yield return www;
}
If your www error is always null (a successful request) then how do you ever get out of this loop? Perhaps this coroutine is still running.
@smoggach: He uses an && between the conditions. So if only one of them is not true it will ter$$anonymous$$ate the loop. However yielding the same www object multiple times is not a good idea at all. If this yield returns the request is done.
@htudrshtsrhtsthsht: You really should use
yield return null;
in this while loop. I'm not familiar with objective C and iOS in general, but how have you actually found the "memory leak"? You are aware of the fact that the managed environment usually keeps some memory reserved for the future. Have you tried executing 20 or more requests in a row with a small delay? If you have a memory leak you should see a s$$anonymous$$dy increase for each call. If it just goes up during the first calls and then stays more or less constant it's not a leak.
Thanks for all the comments and inputs!
@smoggach: yes it is a AND between the statement so once the request is done or there is an error or we have timed out, we stop. Plus when I debug in xcode I can see that the WWW destroy function of unity is called, so the www object are destroyed, which means that we have exited the loop.
@Bunny83: I will try the suggestion to use "yield return null" ins$$anonymous$$d of yield return www, I am not that familiar with the yield statement.
@Bunny83: I found the leak using two things on macOSX: xcode, which has a very neat "Analyze" function that highlight "potential" leak, and Intruments, which is a set à tools available on macOSX that can track memory allocation, and object leaked (see screenshots, it is quite handy). Both of them points me to the WWWConnection class and particularly the UnityStartWWWConnectionGet/POST methods.
(more precisely, I was tracking allocation of the HTTPParser object in Instruments, and it is very noticeable that when they are created, severals are not destroyed (ram usage was always increasing for this object, even after 50+ requests)).
Also I had a s$$anonymous$$dy increase of ram usage (not that much but still noticeable) while all I was doing was http request every $$anonymous$$utes.
After investigating a bit more, I am 100% sure that there is a problem in the two aforementioned methods. I have counted the number of request sent against the number of request disposed, and the difference is always 0, which means that I always call the dispose method on each and every WWW object I use, but when I profile it in Instruments, I am almost sure that there is a leak, because the allocated memory for NFSString and HTTPParser does constantly increase, AND the call stack ALWAYS contains one of the 2 aforementioned methods. I will try to create a small test case to see how to reproduce it.
Alright I have a test case built right here:
You just need to provide your server URL in the AnalyticsLoggerImpl class. I am going to report the bug (when the issue tracker will let me do so)
Answer by htudrshtsrhtsthsht · Oct 27, 2014 at 08:40 AM
All right, it seems that I was not alone:
http://issuetracker.unity3d.com/issues/memory-leak-in-www-class-on-ios
Looks like it's solved in unity 4.5.6, just have to wait for the release. Anyway, thx for the help
Your answer
Follow this Question
Related Questions
@ makes problem in ios 1 Answer
Memory management theory question 1 Answer
Restoring In App Purchases for iOS using Unity 1 Answer
Unity WWW Warning Message in Xcode 1 Answer