{
"$type": "site.standard.document",
"path": "/rubymotion-retain-bug-closure-rm3-workaround/",
"publishedAt": "2013-06-27T07:00:00.000Z",
"site": "at://did:plc:bryys25pc2fnagnyxqgsglhd/site.standard.publication/3mn26bjkkmh23",
"tags": [
"RubyMotion",
"iOS Development",
"Techniques"
],
"textContent": "Update 2013/6/27: RM3 is now fixed as described by Laurent Sansonetti in this RubyMotion Groups post.\n\nUpdate 2014/01/02: Also be careful to use #weak! on procs where appropriate to make the self reference stored in procs weak.\n\nThere's been a bit of a storm over a memory-related crash issue in RubyMotion lately. See Why I'm not using RubyMotion in Production for some background reading.\n\nThe basic problem is that RubyMotion blocks (incorrectly) do not retain objects it closes over. Due to the nature of RubyMotion as well as the nature of Cocoa Touch, which is now heavily block-based, this is a major issue.\n\nThere's a few basic ways to workaround this. But not all of them work.\n\nLet's start with a basic scenario to work on.\n\nImagine you have a function that runs some code asynchronously (this often imply network access such as sending an email, or just POST-ing some data). Often you'll want to wrap Apple's -beginBackgroundTaskWithExpirationHandler: and -endBackgroundTask: around it so the task has a good chance of completing even if the user goes to the home screen or switch to another app. We'll create a #long_running_async_task function that simulates this. #long_running_async_task accepts a block and when the task is completed, it will invoke the block to perform any necessary clean up. We'll call #beginBackgroundTaskWithExpirationHandler right before kickstarting the task and make use of the block to run #endBackgroundTask.\n\nNote that we can't completely avoid block-based APIs since APIs such as #beginBackgroundTaskWithExpirationHandler have no non-block alternatives.\n\nSo we have:\n\nclass AppDelegate\n def application(application,\n didFinishLaunchingWithOptions:launchOptions)\n run_task_with_local_vars\n end\n\n #This is a asynchrous task that calls a block upon completion.\n #Imagine a network fetch or a Parse API call.\n def long_running_async_task(&block)\n gcdq = Dispatch::Queue.new('myqueue')\n gcdq.async {\n p 'Start'\n p 'running...'\n Random.rand(10)+10.times do\n 1000.times do\n 1000*1000\n end\n end\n p 'End, going to run completion block'\n block.call\n p 'After running completion block'\n }\n end\n\n def run_task_with_local_vars\n task_id =\n UIApplication.sharedApplication.\n beginBackgroundTaskWithExpirationHandler(proc {\n return if task_id == UIBackgroundTaskInvalid\n UIApplication.sharedApplication.endBackgroundTask(task_id)\n })\n\n long_running_async_task do\n p 'In block'\n p \"In block with task_id #{task_id}\"\n if task_id != UIBackgroundTaskInvalid\n p \"End task with #{task_id}\"\n UIApplication.sharedApplication.endBackgroundTask(task_id)\n end\n end\n end\nend\n\nIf you run the code above, you'll see that the app crashes right after printing \"In block\". This is due to the retain bug.\n\nAs many have suggested, a possible workaround is to use instance variables. So we have:\n\nclass AppDelegate\n def application(application,\n didFinishLaunchingWithOptions:launchOptions)\n run_task_with_ivar\n\n true\n end\n\n #This is a asynchrous task that calls a block upon completion.\n #Imagine a network fetch or a Parse API call.\n def long_running_async_task(&block)\n gcdq = Dispatch::Queue.new('myqueue')\n gcdq.async {\n p 'Start'\n p 'running...'\n Random.rand(10)+10.times do\n 1000.times do\n 1000*1000\n end\n end\n p 'End, going to run completion block'\n block.call\n p 'After running completion block'\n }\n end\n\n def run_task_with_ivar\n @task_id =\n UIApplication.sharedApplication.\n beginBackgroundTaskWithExpirationHandler(proc {\n return if @task_id == UIBackgroundTaskInvalid\n UIApplication.sharedApplication.endBackgroundTask(@task_id)\n })\n\n long_running_async_task do\n p 'In block'\n p \"In block with task_id #{@task_id}\"\n if @task_id != UIBackgroundTaskInvalid\n p \"End task with #{@task_id}\"\n UIApplication.sharedApplication.endBackgroundTask(@task_id)\n end\n end\n end\nend\n\nThe code above (using ivars) works fine, but if you change #application:didFinishLaunchingWithOptions: to the following and run again?\n\ndef application(application,\n didFinishLaunchingWithOptions:launchOptions)\n run_task_with_ivar\n run_task_with_ivar\n run_task_with_ivar\n run_task_with_ivar\n \n true\nend\n\nYou'll see that the app crashes again with an error message like “Can't endBackgroundTask: no background task exists with identifier 3, or it may have already been ended. Break in UIApplicationEndBackgroundTaskError() to debug.”. This is because the ivar containing the task ID gets overriden as each task is running. (You might need to run it a few times, could be random since we are simulating tasks that don't take the same amount of time to complete). This is very common, for e.g. if user fires off several emails in succession and goes to the homescreen, you'll want this code to work. So only employing ivars don't work.\n\nWhat we need to do is to wrap block based operations into their own classes. We'll create a class WorkaroundTask that provides some basic book-keeping for calling #beginBackgroundTaskWithExpirationHandler and #endBackgroundTask. And create a subclass of WorkaroundTask for each block operation that is affected and move the task code (in our example code that is in #long_running_async_task) into the subclass.\n\nWe get:\n\nclass AppDelegate\n def application(application,\n didFinishLaunchingWithOptions:launchOptions)\n task = MyTask.new\n task.any_info_needed = 'info for task A'\n task.run\n\n true\n end\nend\n\n#Class to work around Proc not capturing as well as not retaining\n#task_id correctly. Subclass to sue. Note that we call #endBackgroundTask,\n#reset the @task_id, release itself when we complete the task or when\n#the expiry handler fires (if we already ended the task, the expiry\n#handler will not fire)\nclass WorkaroundTask\n def dealloc\n p \"Dealloc #{self}\"\n super\n end\n\n def begin_task\n retain\n\n @task_id =\n UIApplication.sharedApplication.\n beginBackgroundTaskWithExpirationHandler(proc {\n if @task_id == UIBackgroundTaskInvalid\n return\n end\n UIApplication.sharedApplication.endBackgroundTask(@task_id)\n @task_id = UIBackgroundTaskInvalid\n release\n })\n end\n\n def end_task\n if @task_id != UIBackgroundTaskInvalid\n p \"End task with #{@task_id}\"\n UIApplication.sharedApplication.endBackgroundTask(@task_id)\n @task_id = UIBackgroundTaskInvalid\n release\n end\n end\nend\n\nclass MyTask < WorkaroundTask\n attr_accessor :any_info_needed\n\n def run\n begin_task\n\n gcdq = Dispatch::Queue.new('myqueue')\n gcdq.async {\n p \"Start and we can use this info in the task: #{any_info_needed}\"\n p 'running...'\n Random.rand(10)+10.times do\n 1000.times do\n 1000*1000\n end\n end\n p 'End, going to run completion block'\n end_task\n p 'After running completion block'\n }\n end\nend\n\nNotice that the tasks runs correctly and #dealloc runs, indicating that clean up is done correctly.\n\nNow if you modify #application:didFinishLaunchingWithOptions: again to run multiple tasks?\n\ndef application(application,\n didFinishLaunchingWithOptions:launchOptions)\n task = MyTask.new\n task.any_info_needed = 'info for task A'\n task.run\n \n task = MyTask.new\n task.any_info_needed = 'info for task B'\n task.run\n \n task = MyTask.new\n task.any_info_needed = 'info for task C'\n task.run\n \n task = MyTask.new\n task.any_info_needed = 'info for task D'\n task.run\n \n true\nend\n\nYou'll notice that this works correctly too. So there you have it. Let's go back to creating wonderful apps that delight our customers!\n\nThanks to Colin T.A. Gray and Matt Green for pointing me in the right direction.\n\nUpdated 2013/07/05: Handle expiry handler correctly\n\nUpdated 2013/07/12: RM3 is now fixed",
"title": "RubyMotion Retain Bug RM3 Workaround"
}