Titanium MapView Not Releasing Memory
I have an app that's built using Appcelerator Titanium (for iOS, latest SDKs of Ti and Apple) and a section of it relies heavily on maps. The problem I'm having is that the memory doesn't seem to be releasing when I close the window containing the MapView. As a result, going back and forth from the menu screen to a map slows down the iPhone until it eventually stops responding altogether (3-5 map loads).
I used Titanium's Ti.Platform.availableMemory
call to see the memory upon entering the window with the map, and after the map is closed. The result is a steady downward trend with each successive entry/exit, along the lines of:
25 (initial load of map.js)
20 (after annotations) 20 (afterwin.close()
)
19 (second load of map.js)
18 (annotations)
19 (leave)
18 (enter)
16 (annotations)
15 (leave)
In the simulator, it may go up a little more when the window's closed, but even it shows a steady downward trend.
Here's the code for the map, which is in it's own "map.js" file. I've trimmed it down to the functionality code that is used (hence why only the event listener for button_index
is here).
button_index.addEventListener('click', function()
{
Ti.App.xhr.abort();
if (mapview) {
mapview.removeAllAnnotations();
mapview = null;
}
if(policeJson){
policeJson = null;
fireJson = null;
}
Ti.App.police = false;
Ti.App.types = null;
win.close(); //This should clean up everything, according to the docs
Ti.API.info('Memory: ' + Ti.Platform.availableMemory);
});
var mapview;// = Ti.App.mapview;
Titanium.Geolocation.purpose = "Recieve User Location";
Titanium.Geolocation.accuracy = Titanium.Geolocation.ACCURACY_BEST;
Ti.API.info('Memory: ' + Ti.Platform.availableMemory);
function getMarkers(e){
var miles = Ti.App.miles;
Ti.API.info("Getting markers");
//Google's API radius is in meters, so we need to convert
var radius = miles * 1610; // 1mi = 1609.344 meters, so we just round up to 1610.
// http connection setup
Ti.App.xhr.setTimeout(10000);
googleLatLng = e.coords.latitude + "," + e.coords.longitude;
Ti.App.xhr.onload = function()
{
var data = Ti.XML.parseString(this.responseText);
var ref = data.documentElement.getElementsByTagName("reference");
if(ref != null && Ti.App.xhr.readyState == 4){
for(var i =0; i < ref.length; i++){
var marker = new Object();
marker.lat = data.documentElement.getElementsByTagName("lat").item(i).text;
marker.lng = data.documentElement.getElementsByTagName("lng").item(i).text;
marker.name = data.documentElement.getElementsByTagName("name").item(i).text;
marker.ref = ref.item(i).text;
addMarker(marker);
marker = null;
}
}
};
Ti.App.xhr.open("GET","https://maps.googleapis.com/maps/api/place/search/xml?location=" + googleLatLng + "&radius=" + radius + "&types=" + Ti.App.types + "&sensor=true&key=" + Ti.App.apiKey,false);
Ti.App.xhr.send();
Ti.API.info('Memory: ' + Ti.Platform.availableMemory);
}
// find the user's location and mark it on the map
function waitForLocation(e)
{
var region = null;
if ( e.error ) {
region = regionDefault; // User didn't let us get their location
var alertDialog = Titanium.UI.createAlertDialog({
title: 'Geolocation',
message: 'We were unable to center the map over your location.',
buttonNames: ['OK']
});
alertDialog.show();
} else {
region = {
latitude: e.coords.latitude,
longitude: e.coords.longitude,
animate:true,
latitudeDelta:0.05,
longitudeDelta:0.05
};
}
Ti.App.lat = region.latitude;
Ti.App.lng = region.longitude;
mapview.setLocation(region);
mapview.removeAllAnnotations();
currentLoc = Titanium.Map.createAnnotation({
latitude: region.latitude,
longitude: region.longitude,
title: e.error ? "Columbus" : "You are here!",
pincolor: Titanium.Map.ANNOTATION_RED,
animate:true
});
mapview.addAnnotation(currentLoc);
mapview.selectAnnotation(currentLoc);
mapview.addEventListener('cl开发者_高级运维ick', function(e){
if (e.clicksource == 'rightButton') {
if (e.annotation.spotUrl != '') {
alert('Website!');
}
else {
alert('No website available');
}
}
});
if(Ti.App.police == true) {
var fire_img = "../../images/iNeighborhood/fire.png";
var police_img = "../../images/iNeighborhood/police.png";
serviceMarkers(fire_addr, fire_title, fire_lat_1, fire_long_1,fire_img);
serviceMarkers(police_addr, police_title, police_lat_1, police_long_1,police_img);
}
getMarkers(e);
}
function addMarker(marker){
var ann = Titanium.Map.createAnnotation({
animate:true,
latitude:marker.lat,
longitude:marker.lng,
title:marker.name,
pincolor: Titanium.Map.ANNOTATION_GREEN
});
mapview.addAnnotation(ann);
}
// Automatically refresh current location.
/*
* IN PROGRESS
*/
function getLocation(){
// create the mapView and center it on Columbus
if (!mapview) {
mapview = Titanium.Map.createView({
mapType: Titanium.Map.STANDARD_TYPE,
animate: true,
region: {
latitude: 39.961176,
longitude: -82.998794,
latitudeDelta: 0.1,
longitudeDelta: 0.1
},
regionFit: true,
userLocation: true,
visible: true,
top:29
});
//Ti.App.mapview = mapview;
win.add(mapview);
}
refresh();
//Get the current position and set it to the mapview
Titanium.Geolocation.getCurrentPosition(waitForLocation);
}
getLocation();
// pretty self explanatory...
function cleanMap(){
if (mapview) {
mapview.removeAllAnnotations();
}
if(xhr){
xhr.abort();
}
}
Ti.App.addEventListener('map:mapIt',function(){
cleanMap();
getLocation();
});
Here's some of the code from the index page that loads the map:
var winMap = Titanium.UI.createWindow({
url:'map.js',
tabBarHidden:false
});
btnEducation.addEventListener('click',function(){
Ti.App.types = Ti.App.schools;
Ti.UI.currentTab.open(winMap);
Ti.App.police = false;
});
I made a global HTTPClient and reuse it, like what some of the other Q&A answers (both on SO and on Appcelerator's site) have suggested, which seems to have helped (not as much memory is drained with each map load), I also tried manually setting the variables (especially the larger ones) to null (which may or may not be effective), but something's still holding on. I also tried creating the map window inside the event listener for the buttons that open the window, but that didn't seem to have any effect at all.
I also ran Instruments to see what it could find and it didn't find anything noteworthy (I even showed it to my coworker, who does mobile development full time and he said there wasn't anything out of the ordinary that he could see).
I've been looking at this code for a few hours now, and it's not all my code, so it's entirely possible I'm missing something obvious, but is there a reason in my code why the memory isn't being released as it should? Is there something else I could do to get more of the memory to release? I'm only developing for iOS right now, so iOS-specific solutions are acceptable.
Edit - I've now also tried including the map portion into the file that calls it (using Ti.include('map.js')
). I made a quick and dirty setup to see if it would work:
Ti.include('map.js');
var button_index = Ti.UI.createButton({
text:'Back',
height:20,
width:50,
top:0,
left:0,
color:'#000'
});
button_index.addEventListener('click', function()
{
Ti.App.xhr.abort();
if (mapview) {
mapview.removeAllAnnotations();
// mapview = null;
}
if(policeJson){
policeJson = null;
fireJson = null;
}
Ti.App.police = false;
Ti.App.types = null;
Ti.App.title = null;
mapview.hide();
Ti.API.info('Memory: ' + Ti.Platform.availableMemory);
});
mapview.add(button_index);
mapview.hide();
btnArts.addEventListener('click',function(){
Ti.App.types = Ti.App.arts;
// Ti.UI.currentTab.open(winMap);
mapview.show();
Ti.App.fireEvent('map:mapIt'); //Triggers the chain of events to clear the map and add the necessary annotations to it
Ti.App.police = false;
Ti.App.title = 'arts';
});
It seems to work better, but there is still a steady decrease in the amount of available memory as I go in and out of the mapview, and the initial memory load makes it as unusable on the devices as the other method (drops memory down to about 3MB).
From the docs regarding tabs/tab groups..."A TabGroup Tab instance. Each Tab instance maintains a stack of tab windows. Only one window within in the Tab can be visible at a time. When a window is closed, either by the user or by code, the window is removed from the stack, make the previous window visible."
One guess is that the close()
when applied to a tab might not act the way you'd assume it would, since it seems to maintain state between the tabs as you cycle between them. Also, maybe there's something missing in the code example above, but I didn't actually see where
"win" was defined as a variable (i'm assuming you have var win = Ti.UI.currentWindow();
somewhere but you might want to double check that it is actually getting closed when that function is called.
You may also look into creating a single object for your application, and chaining the functions to that object, so as not to pollute the global scope. See: http://wiki.appcelerator.org/display/guides/JavaScript+Best+Practices
Is winMap, with its "heavy window" (i.e., pointing to a URL and creating another js context, as opposed to including within the same context) being called each time you go back to the map? I didn't see where it got called from.
are you sure the memory for the mapView is being deallocated? My hunch from looking at the code is that it might be the culprit.
I might suggest using one global mapView object and not keep creating in it map.js
I just wanted to add if you have more projects in the future that are created in Titanium, that there is a recommended way of setting up your application whereby you minimize memory issues.
First, I don't recommend that you use the Ti.include() function. There's a better alternative called require().
I had a number of problems with objects not being garbage collected properly, but these links helped me to write memory-efficient applications:
This is right from Appcelerator: http://search.vimeo.com/29804284#
This explains the require function and the CommonJS modules: https://wiki.appcelerator.org/display/guides/CommonJS+Modules+in+Titanium
An example of how to use CommonJS: https://github.com/appcelerator/Documentation-Examples/blob/master/commonjsExample/Resources/modules/pages/userlist.js
I hope this helps!
精彩评论