{ "id": "138694", "key": "TIMOB-18549", "fields": { "issuetype": { "id": "1", "description": "A problem which impairs or prevents the functions of the product.", "name": "Bug", "subtask": false }, "project": { "id": "10153", "key": "TIMOB", "name": "Titanium SDK/CLI", "projectCategory": { "id": "10100", "description": "Titanium and related SDKs used in application development", "name": "Client" } }, "fixVersions": [], "resolution": { "id": "7", "description": "", "name": "Invalid" }, "resolutiondate": "2015-02-18T21:39:29.000+0000", "created": "2014-10-29T14:17:54.000+0000", "priority": { "name": "Medium", "id": "3" }, "labels": [ "TCSupportTriage", "function", "ios", "ipass1", "kroll", "leak" ], "versions": [ { "id": "16704", "description": "Release 3.5.0", "name": "Release 3.5.0", "archived": false, "released": true, "releaseDate": "2015-01-13" } ], "issuelinks": [], "assignee": { "name": "ingo", "key": "ingo", "displayName": "Ingo Muschenetz", "active": true, "timeZone": "America/Los_Angeles" }, "updated": "2017-05-31T22:45:52.000+0000", "status": { "description": "The issue is considered finished, the resolution is correct. Issues which are closed can be reopened.", "name": "Closed", "id": "6", "statusCategory": { "id": 3, "key": "done", "colorName": "green", "name": "Done" } }, "components": [ { "id": "10206", "name": "iOS", "description": "iOS Platform" } ], "description": "To make it short it is really easy to create a retain cycle on iOS that will prevent proxies from being deallocated, thus creating huge memory leaks\r\n\r\nVery simple example:\r\n\r\nThis won't create a retain cycle\r\n{code:javascript}\r\n var win = Ti.UI.createWindow({\r\n backgroundColor:'red'\r\n });\r\n win.addEventListener('click',function() {\r\n win.close();\r\n });\r\n win.open();\r\n{code}\r\n\r\nThis will (example 1)\r\n{code:javascript}\r\n var win = Ti.UI.createWindow({\r\n backgroundColor:'red'\r\n });\r\n testMe = function() {\r\n win.close();\r\n }\r\n win.addEventListener('click',testMe);\r\n win.open();\r\n{code}\r\nSame as this (example 2)\r\n{code:javascript}\r\n var win = Ti.UI.createWindow({\r\n backgroundColor:'red'\r\n });\r\n win.testMe = function() {\r\n win.close();\r\n }\r\n win.addEventListener('click',win.testMe);\r\n win.open();\r\n{code}\r\n\r\nYou can simply test it by adding a breakpoint in TiWindowProxy dealloc method.\r\nYou will break in the first example after clicking the window. You won't in the second and third\r\n\r\nThe reason is actually pretty simple:\r\n* example1 : the top context holds a reference to testMe\r\ntestMe's context holds a reference to win.\r\nThus win can't be dealloc, then testMe can't be dealloc ....\r\n* example2:win holds a reference to testMe. testMe's context holds a reference to win. => retain cycle\r\n\r\nThose 2 examples simply a very important retain cycle in the SDK.\r\nThis should be handled to prevent such things.\r\nI have been wrapping my head around this one but it's not easy to fix\r\n", "attachment": [], "flagged": false, "summary": "iOS: KrollBidge, reference and memory leak", "creator": { "name": "farfromrefuge", "key": "farfromrefuge", "displayName": "Martin Guillon", "active": false, "timeZone": "Europe/Berlin" }, "subtasks": [], "reporter": { "name": "farfromrefuge", "key": "farfromrefuge", "displayName": "Martin Guillon", "active": false, "timeZone": "Europe/Berlin" }, "environment": "Ti SDK 3.5.0 master\r\nios sdk 8\r\nxcode 6.1", "comment": { "comments": [ { "id": "329907", "author": { "name": "fokkezb", "key": "fokke", "displayName": "Fokke Zandbergen", "active": true, "timeZone": "Europe/Amsterdam" }, "body": "Is your second example missing the {{var}} for {{testMe}} on purpose or is that a typo?\r\n\r\nThe third example is discouraged by both docs and community.", "updateAuthor": { "name": "fokkezb", "key": "fokke", "displayName": "Fokke Zandbergen", "active": true, "timeZone": "Europe/Amsterdam" }, "created": "2014-10-29T14:53:48.000+0000", "updated": "2014-10-29T14:53:48.000+0000" }, { "id": "329908", "author": { "name": "farfromrefuge", "key": "farfromrefuge", "displayName": "Martin Guillon", "active": false, "timeZone": "Europe/Berlin" }, "body": "@Fokke: Not on purpose no, but should have the same effect if i am not mistaken.\r\nThe third example might be discouraged, but sometimes you can't go around it. Using the commonly used \"var that = this\" way.\r\nI think this could be handled by the framework. most browser now handles javascript scope and closure.", "updateAuthor": { "name": "farfromrefuge", "key": "farfromrefuge", "displayName": "Martin Guillon", "active": false, "timeZone": "Europe/Berlin" }, "created": "2014-10-29T15:02:49.000+0000", "updated": "2014-10-29T15:02:49.000+0000" }, { "id": "329909", "author": { "name": "skypanther", "key": "skypanther", "displayName": "Tim Poulsen", "active": true, "timeZone": "America/New_York" }, "body": "This is a standard JavaScript variable scoping issue, not a Titanium issue. You're creating a global variable (testMe in example1) that holds a function that references a local object. Garbage collection will never clean up testMe (globals can't be garbage collected). You don't set it to null anywhere. So, it remains valid and since it refers to win, that object must also remain valid throughout the execution of your app.\r\n\r\nExample 2 likewise sets a listener which refers to a local object, which you don't de-reference later. Thus, it too creates an un-garbage-collectible reference. (Though this one could be cleaned up depending on how that window is opened and closed. But in your simple one-window app it won't be.)\r\n\r\nI don't see how Titanium can (or should) catch such coding structural issues to fix them for you. You must manage scope, null variables, and remove listeners as appropriate to your code or you will create leaks. (The same is true in a Node.js or web JavaScript environment.)", "updateAuthor": { "name": "skypanther", "key": "skypanther", "displayName": "Tim Poulsen", "active": true, "timeZone": "America/New_York" }, "created": "2014-10-29T15:04:28.000+0000", "updated": "2014-10-29T15:04:28.000+0000" }, { "id": "329911", "author": { "name": "farfromrefuge", "key": "farfromrefuge", "displayName": "Martin Guillon", "active": false, "timeZone": "Europe/Berlin" }, "body": "@Tim: I guess you are right about the first one. At first it was not part of that ticket, i added after but maybe i shouldn't have.\r\nAs for the second, you understand that the \"addEventListener\" has nothing to do with it, right? It s just there to close the window. \r\nThis would work too \r\n{code:javascript}\r\nvar win = Ti.UI.createWindow({\r\n backgroundColor:'red'\r\n});\r\nwin.testMe = function() {\r\n win.close();\r\n}\r\nwin.addEventListener('click',function() {win.close();}); // this does NOT create a memory leak\r\nwin.open();\r\n{code}\r\nThe retain cycle simplycomes from that code\r\n{code:javascript}\r\nwin.testMe = function() {\r\n win.close();\r\n}\r\n{code}\r\nAnd i think this one can be handled", "updateAuthor": { "name": "farfromrefuge", "key": "farfromrefuge", "displayName": "Martin Guillon", "active": false, "timeZone": "Europe/Berlin" }, "created": "2014-10-29T15:09:29.000+0000", "updated": "2014-10-29T15:09:53.000+0000" }, { "id": "329916", "author": { "name": "skypanther", "key": "skypanther", "displayName": "Tim Poulsen", "active": true, "timeZone": "America/New_York" }, "body": "You can handle the second by removing the event listener, like this:\r\n\r\n{code}\r\nvar win = Ti.UI.createWindow({\r\n backgroundColor:'red'\r\n});\r\nvar testMe = function() {\r\n win.removeEventListener('click',testMe);\r\n win.close();\r\n}\r\nwin.addEventListener('click',testMe);\r\nwin.open();\r\n{code}", "updateAuthor": { "name": "skypanther", "key": "skypanther", "displayName": "Tim Poulsen", "active": true, "timeZone": "America/New_York" }, "created": "2014-10-29T15:19:28.000+0000", "updated": "2014-10-29T15:19:28.000+0000" }, { "id": "329920", "author": { "name": "farfromrefuge", "key": "farfromrefuge", "displayName": "Martin Guillon", "active": false, "timeZone": "Europe/Berlin" }, "body": "Did you test it? Cause it should not work.\r\naddEventListener has nothing to do with the retain cycle.\r\nTo actually fix it you need to set testMe to null, that way the global context releases testMe, then win can be released.", "updateAuthor": { "name": "farfromrefuge", "key": "farfromrefuge", "displayName": "Martin Guillon", "active": false, "timeZone": "Europe/Berlin" }, "created": "2014-10-29T15:28:53.000+0000", "updated": "2014-10-29T15:28:53.000+0000" }, { "id": "342760", "author": { "name": "bobrosoft", "key": "bobrosoft", "displayName": "Vladimir Tolstikov", "active": true, "timeZone": "Europe/Moscow" }, "body": "Hey, we have the same issue here with new SDKs (using code near as in \"example 2\"), but *it definitely was ok in old SDKs, I can prove by video, if needed*. I checked through Xcode Instruments and found that in new SDKs after 3.1.3 (can't say from which one exactly it started, was on 3.1.3 for a long time) that assignment now creates memory leaks. In 3.1.3 and before it performed correctly and windows (and other proxies) have been deallocated properly. Moved back to 3.1.3 because of that issue and need to check and update huge amount of codebase where that handy stuff used before we can switch to latest SDK :(", "updateAuthor": { "name": "bobrosoft", "key": "bobrosoft", "displayName": "Vladimir Tolstikov", "active": true, "timeZone": "Europe/Moscow" }, "created": "2015-02-10T10:17:42.000+0000", "updated": "2015-02-10T10:17:42.000+0000" }, { "id": "342932", "author": { "name": "bobrosoft", "key": "bobrosoft", "displayName": "Vladimir Tolstikov", "active": true, "timeZone": "Europe/Moscow" }, "body": "To be clear, my case, which worked in older SDKs, is:\r\n\r\n{code}\r\nfunction MyWindow()\r\n{\r\n var self = Ti.UI.createWindow({\r\n backgroundColor:'red'\r\n });\r\n\r\n // Assign external props and methods\r\n self.myMethod = myMethod; // <-- here's memory leak now. Was OK in 3.1.3\r\n \r\n return self;\r\n\r\n /**\r\n * Changes background color\r\n * @public\r\n */\r\n function myMethod()\r\n {\r\n self.backgroundColor = 'blue';\r\n }\r\n}\r\n\r\nmodule.exports = MyWindow;\r\n{code}\r\n\r\nThat was very handy, simple and easy-to-use pattern we used to write custom UI elements. Now it creates leaks and we will need to change a good amount of code across the app to avoid assigning public \"methods\" directly to proxies.", "updateAuthor": { "name": "bobrosoft", "key": "bobrosoft", "displayName": "Vladimir Tolstikov", "active": true, "timeZone": "Europe/Moscow" }, "created": "2015-02-11T18:08:35.000+0000", "updated": "2015-02-11T18:13:53.000+0000" }, { "id": "343819", "author": { "name": "penrique", "key": "penrique", "displayName": "Pedro Enrique", "active": false, "timeZone": "America/Los_Angeles" }, "body": "It has always been a bad practice to add javascript functions to Titanium objects directly. Although a proxy (in this case the window) has a JS object counterpart, this is not a regular JS object. We have some internal hooks to check for properties, methods, etc.. used for the binding to the native classes.\r\nThat said, the problem is really what Tim mentioned about the circular reference. I did some testing and using \"this\" instead of the var itself works as expected. This would only work if the only variable that you're trying to use is the one attached. For example:\r\nh4. Quick workaround\r\n{code}\r\nvar self = Ti.UI.createWindow({\r\n backgroundColor:'red'\r\n});\r\nself.myMethod = function() {\r\n this.backgroundColor = 'blue';\r\n};\r\n{code}\r\nNow, the proper way to do this in JavaScript would be to create an object that contains those UI components and return that instead of the Window object. Something like this\r\nh4. Better approach #1\r\n{code}\r\nfunction MyWindow() {\r\n var self = {};\r\n var win = Ti.UI.createWindow();\r\n // more code...\r\n self.open = function() {\r\n win.open();\r\n };\r\n self.close = function() {\r\n win.close();\r\n };\r\n self.replaceColor = function(_color) {\r\n win.backgroundColor = _color;\r\n };\r\n return self;\r\n}\r\n{code}\r\nOr you can always go a bit further an be more object-oriented\r\nh4. Better approach #2\r\n{code}\r\n// Create this window using \"new MyWindow()\"\r\nfunction MyWindow() {\r\n\tthis.win = Ti.UI.createWindow();\r\n}\r\nMyWindow.prototype.open = function() {\r\n\tthis.win.open();\r\n};\r\nMyWindow.prototype.close = function() {\r\n\tthis.win.close();\r\n};\r\nMyWindow.prototype.replaceColor = function(_color) {\r\n\tthis.win.backgroundColor = _color;\r\n};\r\n{code}", "updateAuthor": { "name": "penrique", "key": "penrique", "displayName": "Pedro Enrique", "active": false, "timeZone": "America/Los_Angeles" }, "created": "2015-02-18T21:06:51.000+0000", "updated": "2015-02-18T21:06:51.000+0000" }, { "id": "343828", "author": { "name": "penrique", "key": "penrique", "displayName": "Pedro Enrique", "active": false, "timeZone": "America/Los_Angeles" }, "body": "Resolving this ticket as invalid. The issue presented is a due to the way JavaScript and GC works. Variables declared outside of a function and referenced in that function will be retained as long as that function is available for usage.", "updateAuthor": { "name": "penrique", "key": "penrique", "displayName": "Pedro Enrique", "active": false, "timeZone": "America/Los_Angeles" }, "created": "2015-02-18T21:39:29.000+0000", "updated": "2015-02-18T21:39:29.000+0000" }, { "id": "343895", "author": { "name": "farfromrefuge", "key": "farfromrefuge", "displayName": "Martin Guillon", "active": false, "timeZone": "Europe/Berlin" }, "body": "@Pedro: How can you mark that as invalid :D Your examples solve Vladimir cases, but not what i presented.\r\nAnd it has nothing to do with variables declared outside a function. It s related to contexts and mutual retains of variables in context making it impossible to release an object.\r\n", "updateAuthor": { "name": "farfromrefuge", "key": "farfromrefuge", "displayName": "Martin Guillon", "active": false, "timeZone": "Europe/Berlin" }, "created": "2015-02-19T08:10:52.000+0000", "updated": "2015-02-19T08:10:52.000+0000" }, { "id": "343898", "author": { "name": "bobrosoft", "key": "bobrosoft", "displayName": "Vladimir Tolstikov", "active": true, "timeZone": "Europe/Moscow" }, "body": "@Pedro, thanks for answer.\r\nYes, currently we're going to switch on something near \"Better approach #1\", but I found a common case which needs to be solved in some way. The case is next:\r\n - we have list (TableView or ScrollView for example) of custom UI elements\r\n - we have one \"click\" event listener linked to whole list\r\n - from a list item clicked: need to get my custom instance from it or need to get additional information\r\n\r\nBefore we used next concept:\r\n{code}\r\nfunction onClick(e)\r\n{\r\n Ti.API.info(e.source.getMyCustomData()); // e.source -- was my custom instance\r\n}\r\n{code}\r\nbut now, since assigning of functions to proxies creates memory leaks, how we should do that? If we go with \"Better approach #1\" and no able to assign any custom property to proxies, how could I find the information I need or to get my instance (self) from e.source? I seen that assigning of non-functions to proxies seems not create memory leaks (yet), but if we want to \"go clear\", that should not be used as well in any form, no?\r\n\r\nAlso, another big disadvantage of new approach: no possibility to listen/fire events. There's no (or I not found) way to inherit your object from TiProxy or something that will provide addEventListener, removeEventListener and fireEvent. Currently, to do that possible, we're creating dummy TiUIView (as \"self\") which not used in UI, but only provide that events mechanism.\r\n\r\nWould be very grateful if you can drop some good sample code as answer on questions above.\r\n\r\nAnd some words about my initial comment: that would be good if that memory leak will be solved again as it was in old SDKs -- should be more stable for everybody, isn't that a good goal? :)", "updateAuthor": { "name": "bobrosoft", "key": "bobrosoft", "displayName": "Vladimir Tolstikov", "active": true, "timeZone": "Europe/Moscow" }, "created": "2015-02-19T10:48:31.000+0000", "updated": "2015-02-19T11:05:18.000+0000" }, { "id": "343902", "author": { "name": "skypanther", "key": "skypanther", "displayName": "Tim Poulsen", "active": true, "timeZone": "America/New_York" }, "body": "[~farfromrefuge] Please see my comment from 29/Oct/14. You're creating global variables that reference local variables and not cleaning up. As I stated, this is a coding error, not a Titanium issue. You need to clean up (null out) variables and avoid globals. I agree, the discussion later digressed to proxies and so forth. But I think I already addressed your initial concerns.", "updateAuthor": { "name": "skypanther", "key": "skypanther", "displayName": "Tim Poulsen", "active": true, "timeZone": "America/New_York" }, "created": "2015-02-19T14:58:11.000+0000", "updated": "2015-02-19T14:58:11.000+0000" }, { "id": "343907", "author": { "name": "farfromrefuge", "key": "farfromrefuge", "displayName": "Martin Guillon", "active": false, "timeZone": "Europe/Berlin" }, "body": "Tim Poulsen: It doesnt change anything as i already answered:\r\n* i don't want to remove the event listener in \"testMe\" which means \"win\" will still hold a reference to testMe\r\n* even if i put testMe to null, testMe will still exist (as reference in addEventListener) and thus testMe will still hold a reference to win in its context\r\n-> The retain cycle is still there even if i put testMe to null.\r\n\r\nThat ticket is not about how i format the javascript code. But you are right in the sense that the example code was not perfect :D", "updateAuthor": { "name": "farfromrefuge", "key": "farfromrefuge", "displayName": "Martin Guillon", "active": false, "timeZone": "Europe/Berlin" }, "created": "2015-02-19T16:27:59.000+0000", "updated": "2015-02-19T16:27:59.000+0000" }, { "id": "355802", "author": { "name": "ingo", "key": "ingo", "displayName": "Ingo Muschenetz", "active": true, "timeZone": "America/Los_Angeles" }, "body": "[~emerriman] removed fix version.", "updateAuthor": { "name": "ingo", "key": "ingo", "displayName": "Ingo Muschenetz", "active": true, "timeZone": "America/Los_Angeles" }, "created": "2015-06-23T20:23:16.000+0000", "updated": "2015-06-23T20:23:16.000+0000" }, { "id": "420729", "author": { "name": "lmorris", "key": "lmorris", "displayName": "Lee Morris", "active": false, "timeZone": "America/Los_Angeles" }, "body": "Closing ticket as invalid.", "updateAuthor": { "name": "lmorris", "key": "lmorris", "displayName": "Lee Morris", "active": false, "timeZone": "America/Los_Angeles" }, "created": "2017-05-31T22:45:52.000+0000", "updated": "2017-05-31T22:45:52.000+0000" } ], "maxResults": 18, "total": 18, "startAt": 0 } } }